Files
StarPilot/openpilot/selfdrive/car/tests/test_lkas_long_only.py
T
firestar5683 80fbb4f6db Feature registration
Register vehicle controls, planner modes, UI actions and Galaxy services after their providers exist. Wire the shared runtime entrypoints and cover their combined behavior.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Kacper Rączy <gfw.kra@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: stef <19478336+stefpi@users.noreply.github.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: haraschax <6804392+haraschax@users.noreply.github.com>
Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

142 lines
7.5 KiB
Python

"""The physical LKAS gesture must stop lateral output during ordinary engagement."""
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from opendbc.can import CANParser
from opendbc.car import Bus
from opendbc.car.hyundai.tests.test_ioniq6_longitudinal import controller_fixture
from opendbc.car.hyundai.values import DBC
from opendbc.car.interfaces import RadarInterfaceBase
from opendbc.car.structs import car
from openpilot.cereal import log, messaging
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD
from openpilot.starpilot.aol.wire import SafetyState, decode_intent, encode_safety
from openpilot.starpilot.audio.axis_alerts import AudibleAlert, AxisAlerts
from openpilot.starpilot.ui.onroad import axis_status_color
from openpilot.starpilot.ui.onroad_state import OnroadState, SpeedLimitObservation
@pytest.mark.parametrize('alt', [False, True])
@pytest.mark.parametrize('prearmed', [False, True])
def test_full_lkas_long_only_actual_owners_output_and_presentation(alt, prearmed):
with OpenpilotPrefix(), patch.dict('os.environ', {'SIMULATION': '1'}):
messaging.reset_context()
cp, controller_cs, controller = controller_fixture(alt, aol=True)
params = Params()
params.put_bool('OpenpilotEnabledToggle', True, block=True)
params.put_bool('AlwaysOnLateral', True, block=True)
cs = car.CarState(canValid=True, gearShifter=car.CarState.GearShifter.drive, vEgo=20.)
cs.cruiseState.available = True
class Interface:
CP, CC, CS = cp, controller, SimpleNamespace()
def update(self, _can):
return cs
class Radar(RadarInterfaceBase):
def update(self, can_packets):
return None
card = Car(Interface(), Radar(cp))
sd, controls = SelfdriveD(), Controls()
assert card.aol_card_intent.explicit_latch and sd.aol_replay and controls.aol_replay
sd.initialized = True
sd.state_machine.state = log.SelfdriveState.OpenpilotState.enabled
audio = AxisAlerts()
audio_sm = messaging.SubMaster(['aolIntentWire', 'selfdriveState'])
now = 10_000_000_000
captured = {}
def send(service, message):
captured[service] = messaging.log_from_bytes(message.to_bytes())
vars(card)['pm'] = SimpleNamespace(send=send)
vars(sd)['pm'] = SimpleNamespace(send=send)
card.CC_prev = car.CarControl(enabled=True, latActive=True, longActive=True)
vars(card.aol_card_intent)['allowed_latch'] = prearmed
def tick(pressed, expected_lateral, *, native_lateral=True):
nonlocal now
now += 10_000_000
cs.buttonEvents = [] if pressed is None else [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=pressed)]
cc_message = messaging.new_message('carControl', valid=True)
cc_message.logMonoTime = now
cc_message.carControl = card.CC_prev
card.sm.update_msgs(now / 1e9, [cc_message.as_reader()])
with (patch('openpilot.selfdrive.car.card.messaging.drain_sock_raw', return_value=[b'can']),
patch('openpilot.selfdrive.car.card.can_capnp_to_list', return_value=[]),
patch.object(card.sm, 'update'), patch.object(card, 'observe_ioniq6_long_authority'),
patch('openpilot.cereal.messaging.time.monotonic', return_value=now / 1e9),
patch('openpilot.selfdrive.car.card.time.monotonic_ns', return_value=now)):
parsed, radar = card.state_update()
card.state_publish(parsed, radar)
intent = decode_intent(captured['aolIntentWire'].aolIntentWire)
assert intent.pauseLateral is not expected_lateral
assert not intent.pauseLongitudinal
safety = messaging.new_message('aolSafetyWire', 0, valid=True)
safety.logMonoTime = now
safety.aolSafetyWire = encode_safety(SafetyState(
1, True, now, now + 200_000_000, int(cp.safetyConfigs[0].safetyModel.raw), cp.safetyConfigs[0].safetyParam,
native_lateral, True, native_lateral, True, 'panda', sd.aol_session_id))
calibration = messaging.new_message('extrinsicsCalibration', valid=True)
calibration.extrinsicsCalibration.calStatus = log.ExtrinsicsCalibration.Status.calibrated
sd.sm.update_msgs(now / 1e9, [captured['aolIntentWire'], safety.as_reader(), calibration.as_reader()])
def sampled_car_state():
event = captured['carState']
# This mock replaces the socket sampler, so carry the exact published
# envelope validity and original source stamp into its admission flags.
assert event.valid and event.carState.canValid and not event.carState.canTimeout
assert event.logMonoTime == intent.carStateLogMonoTime == now
sd.aol_car_state_log_ns = int(event.logMonoTime)
sd.conditional_car_state_valid = bool(event.valid)
return event.carState
with (patch.object(sd, 'data_sample', side_effect=sampled_car_state),
patch.object(sd, 'update_events', side_effect=lambda _: sd.events.clear()),
patch.object(sd.sm, 'all_checks', return_value=True),
patch('openpilot.cereal.messaging.time.monotonic', return_value=now / 1e9),
patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=now)):
sd.step()
decision = sd.aol_axis_decision
assert decision.desired_lateral == expected_lateral
assert decision.desired_longitudinal and decision.longitudinal_active
assert decision.lateral_active == (expected_lateral and native_lateral)
controls.sm.update_msgs(now / 1e9, [captured['carState'], captured['selfdriveState'], captured['aolAxisState'], safety.as_reader()])
with patch('openpilot.selfdrive.controls.controlsd.time.monotonic_ns', return_value=now):
command, _ = controls.state_control()
assert command.enabled and command.longActive
assert command.latActive == decision.lateral_active
if not expected_lateral:
assert command.actuators.torque == 0.
_, frames = controller.update(command.as_reader(), controller_cs, now)
parser = CANParser(DBC[cp.carFingerprint][Bus.pt], [('LFA', 0)], 1)
parser.update((now, [next(frame for frame in frames if frame[0] == 0x12A)]))
assert parser.vl['LFA']['ActToiSta'] == int(command.latActive)
if not expected_lateral:
assert parser.vl['LFA']['StrTqReqVal'] == 0
display = OnroadState(engaged=True, camera_available=False, speed_mps=20., cruise_kph=100.,
speed_limit=SpeedLimitObservation(), lateral_active=command.latActive, longitudinal_active=command.longActive)
color = axis_status_color(display)
assert (color.r, color.g, color.b) == ((22, 127, 64) if command.latActive else (255, 105, 180))
audio_sm.update_msgs(now / 1e9, [captured['aolIntentWire'], captured['selfdriveState']])
sound = audio.update(audio_sm, AudibleAlert.none, now)
card.CC_prev = command
controller.frame += controller.frame % 2 # LFA is transmitted at 50 Hz.
return sound
assert tick(None, True) == AudibleAlert.none # Neutral baseline, fully engaged.
assert tick(True, False) == AudibleAlert.disengage
assert tick(True, False) == AudibleAlert.none # A held press cannot rearm.
assert tick(False, False, native_lateral=False) == AudibleAlert.none
# Old long-only native acknowledgment cannot grant new lateral output.
assert tick(True, True, native_lateral=False) == AudibleAlert.engage
assert tick(False, True) == AudibleAlert.none