Files
StarPilot/openpilot/starpilot/lateral/tests/test_lane_runtime.py
T
2026-10-26 20:13:26 -05:00

1057 lines
57 KiB
Python

"""Saved preferences reach actual Controls only with fresh lateral authority."""
import os
import tempfile
from pathlib import Path
from types import SimpleNamespace
import unittest
from unittest import mock
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.starpilot.lateral.lane_centering import ControlMode
from openpilot.starpilot.lateral.lane_runtime import LaneCenteringHost, read_settings, runtime_supported
from openpilot.starpilot.lateral.tests.test_lane_centering import model
from openpilot.starpilot.aol.wire import SafetyState, encode_safety
from opendbc.car import gen_empty_fingerprint
from opendbc.car.car_helpers import interfaces
from opendbc.car.honda.interface import CarInterface as HondaCarInterface
from opendbc.car.honda.values import CAR as HondaCAR
from opendbc.car.hyundai.interface import CarInterface as HyundaiCarInterface
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 opendbc.car.toyota.values import CAR
def ioniq_candidate(*, alternate=False):
fingerprint = gen_empty_fingerprint()
fingerprint[2].update({0x110: 32, 0x362: 32} if alternate else {0x50: 16, 0x2A4: 24})
fingerprint[1].update({0x1CF: 8, 0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24,
0x1BA: 24, 0x1E5: 16, 0x36A: 16})
fingerprint[0][0x3A5] = 24
fingerprint[0][0x100] = 24
stock = HyundaiCarInterface.get_params(HyundaiCAR.HYUNDAI_IONIQ_6, fingerprint, [], False, False, False)
return stock, build_ioniq6_hda2_long_candidate(stock, fingerprint)
def known_body_fixture():
# Documented pre-facelift body bounds for control-path tests. This injects a
# known body; it does not certify a production VIN or firmware/body mapping.
return mock.patch.object(HyundaiCarInterface, 'get_lateral_body_dimensions', create=True, return_value=(1.88, 4.855))
class Frame:
def __init__(self, now_ns):
self.logMonoTime = dict.fromkeys(LaneCenteringHost.REQUIRED, now_ns)
self.checked = True
self.state = SimpleNamespace(canValid=True, canTimeout=False)
def all_checks(self, _services):
return self.checked
def all_alive(self, _services):
return True
def all_valid(self, _services):
return True
def __getitem__(self, _service):
return self.state
def publish_corolla_params(params):
"""Use Card's real startup admission and serialized full-axis configuration."""
from openpilot.selfdrive.car.card import Car
params.put_bool('OpenpilotEnabledToggle', True, block=True)
params.put_bool('SafeMode', False, block=True)
cp = interfaces[CAR.TOYOTA_COROLLA_TSS2].get_non_essential_params(CAR.TOYOTA_COROLLA_TSS2)
def discover(*args, pre_create_hook, **kwargs):
return interfaces[cp.carFingerprint](pre_create_hook(cp.as_reader().as_builder(), cp.carFingerprint, {}, []))
with mock.patch.dict(os.environ, {'SIMULATION': '1'}), \
mock.patch('openpilot.selfdrive.car.card.messaging.recv_one_retry', return_value=SimpleNamespace(can=[1])), \
mock.patch('openpilot.selfdrive.car.card.get_car', side_effect=discover):
selected = Car()
final = selected.CP.as_reader().as_builder()
assert final.alternativeExperience == 32
return final
def feed(controls, timestamp, tick, *, active=True, enabled=True, fault=False, override=False, signal=False, model_age=0,
can_valid=True, can_timeout=False, aol_lat_only=False, native_session='lane-aol-session', speed=20.,
drive_id=500_000_000, native_allowed=True, independent=False, delay_fast=False):
events = []
def msg(service, size=None):
event = messaging.new_message(service, size, valid=True, logMonoTime=timestamp)
events.append(event)
return getattr(event, service)
device = msg('deviceState')
device.started = True
device.startedMonoTime = drive_id
cs = msg('carState')
cs.vEgo = cs.vEgoRaw = speed
cs.vCruise = cs.vCruiseCluster = 100.0
cs.canValid = can_valid
cs.canTimeout = can_timeout
cs.steerFaultTemporary = fault
cs.steeringPressed = override
cs.leftBlinker = signal
state = msg('selfdriveState')
state.active, state.enabled = active, enabled
state.state = 'enabled' if enabled else 'disabled'
if tick % 5 == 0:
lp = msg('vehicleParameters')
lp.stiffnessFactor, lp.steerRatio = 1.0, controls.CP.steerRatio
msg('longitudinalPlan').aTarget = 0.5
if tick % 25 == 0 or delay_fast:
msg('lateralDelay').lateralDelay = 0.1
msg('onroadEvents', 0)
if tick % 5 == 0:
event = messaging.new_message('modelV2', valid=True, logMonoTime=timestamp - model_age)
event.modelV2 = model()
event.modelV2.action.desiredCurvature = 0.0002
events.append(event)
if aol_lat_only:
axis = msg('aolAxisState')
axis.qualified = axis.nativeAcknowledged = axis.desiredLateral = axis.lateralActive = True
axis.desiredLongitudinal = axis.longitudinalActive = False
axis.sessionId = 'lane-aol-session'
axis.observedMonoTime = timestamp
axis.validUntilMonoTime = timestamp + 30_000_000
msg('aolSafetyWire', 0)
events[-1].aolSafetyWire = encode_safety(SafetyState(
1, True, timestamp, timestamp + 200_000_000, int(car.CarParams.SafetyModel.hondaBosch),
controls.CP.safetyConfigs[-1].safetyParam, True, False, True, False,
'lane-test-panda', native_session))
elif controls.aol_replay and controls.CP.brand == 'toyota':
from openpilot.starpilot.car.toyota.aol import qualified
assert qualified(controls.CP, marked_only=True)
axis = msg('aolAxisState')
axis.qualified = axis.nativeAcknowledged = True
axis.desiredLateral = axis.lateralActive = independent or active and enabled
axis.desiredLongitudinal = axis.longitudinalActive = enabled
axis.sessionId = native_session
axis.observedMonoTime = timestamp
axis.validUntilMonoTime = timestamp + 30_000_000
msg('aolSafetyWire', 0)
events[-1].aolSafetyWire = encode_safety(SafetyState(
1, True, timestamp, timestamp + 200_000_000, int(controls.CP.safetyConfigs[0].safetyModel.raw),
controls.CP.safetyConfigs[0].safetyParam, native_allowed and can_valid and not can_timeout and not fault, can_valid and not can_timeout,
independent or active and enabled, enabled, 'lane-test-panda', native_session))
controls.sm.update_msgs(timestamp / 1e9, [event.as_reader() for event in events])
class LaneRuntimeTests(unittest.TestCase):
def test_body_profile_reaches_actual_controls_and_clearance_withdraws_only_added_curvature(self):
from opendbc.car.hyundai.tests.test_lane_body import identity
with OpenpilotPrefix(), mock.patch.dict(os.environ, {'LANE_CENTERING_REPLAY_RUNTIME': '0', 'REPLAY': '1',
'AOL_REPLAY_RUNTIME': '0'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
_, cp = ioniq_candidate()
metadata = identity()
cp.carVin = metadata.carVin
cp.carFw = [vars(fw) for fw in metadata.carFw]
before = cp.to_dict()
params.put('CarParams', cp.to_bytes(), block=True)
params.put_bool('LaneCentering', True, block=True)
params.put('LaneCenteringE2EAuthority', 0., block=True)
baseline, candidate = Controls(), Controls(predict_lane_clearance=True)
baseline.lane_centering_host = None
body = candidate.lane_centering_controller._body
self.assertIsNotNone(body)
self.assertEqual((body.width, body.length, body.height), (1.88, 4.855, 1.495))
self.assertEqual(candidate.CP.to_dict(), before)
original_model = model
phase = 0
def perceived_model():
m = original_model(left=-1.2, right=2.4)
m.init('roadEdges', 2)
for edge, side in zip(m.roadEdges, (-1., 1.), strict=True):
edge.x, edge.y = list(range(51)), [10. * side] * 51
m.roadEdgeStds = [.1, .1]
if phase >= 30:
m.laneLineProbs = [0., .9, .1, 0.]
if 60 <= phase < 90:
m.roadEdges[1].y = [1.8] * 51
return m
added = recovered = 0
with mock.patch(f'{__name__}.model', side_effect=perceived_model):
for phase in range(150):
now = 2_000_000_000 + phase * 10_000_000
override = phase >= 120
feed(baseline, now, phase, override=override)
feed(candidate, now, phase, override=override)
reference, _ = baseline.state_control()
command, _ = candidate.state_control()
self.assertEqual((command.latActive, command.longActive), (reference.latActive, reference.longActive))
self.assertEqual(command.actuators.accel, reference.actuators.accel)
self.assertEqual(command.actuators.longControlState, reference.actuators.longControlState)
if 60 <= phase < 90 or override:
self.assertEqual(candidate.lane_centering_applied, 0.)
elif 30 <= phase < 60:
added += abs(candidate.lane_centering_applied) > 1e-6
elif 90 <= phase < 120:
recovered += abs(candidate.lane_centering_applied) > 1e-6
self.assertGreater(added, 0)
self.assertGreater(recovered, 0)
self.assertEqual(candidate.last_lane_centering_result.reason, 'driver_override')
def test_unqualified_body_through_actual_controls_preserves_nominal_native_output(self):
for identity in ('absent', 'manual', 'unknown_firmware', 'mismatched_firmware'):
with self.subTest(identity=identity), OpenpilotPrefix(), \
mock.patch.dict(os.environ, {'LANE_CENTERING_REPLAY_RUNTIME': '0', 'REPLAY': '1', 'AOL_REPLAY_RUNTIME': '0'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
_, cp = ioniq_candidate()
if identity == 'manual':
cp.fingerprintSource = 'fixed'
elif identity in ('unknown_firmware', 'mismatched_firmware'):
cp.fingerprintSource = 'fw'
firmware = [{'ecu': 'fwdCamera', 'address': 0x7c4, 'brand': 'hyundai', 'fwVersion': b'UNQUALIFIED'}]
if identity == 'mismatched_firmware':
firmware.append({'ecu': 'fwdRadar', 'address': 0x7d0, 'brand': 'hyundai',
'fwVersion': b'\xf1\x00CE__ RDR ----- 1.00 1.01 99110-KL000 '})
cp.carFw = firmware
params.put('CarParams', cp.to_bytes(), block=True)
params.put_bool('LaneCentering', True, block=True)
params.put('LaneCenteringE2EAuthority', 0., block=True)
baseline, candidate = Controls(), Controls(predict_lane_clearance=True)
baseline.lane_centering_host = None
for tick in range(40):
now = 2_000_000_000 + tick * 10_000_000
feed(baseline, now, tick)
feed(candidate, now, tick)
reference, _ = baseline.state_control()
command, _ = candidate.state_control()
self.assertEqual((command.latActive, command.longActive), (reference.latActive, reference.longActive))
self.assertEqual(command.actuators.to_dict(), reference.actuators.to_dict())
self.assertEqual(candidate.desired_curvature, baseline.desired_curvature)
self.assertEqual(candidate.lane_centering_applied, 0.)
self.assertTrue(command.latActive)
self.assertNotEqual(candidate.desired_curvature, 0.)
self.assertTrue(candidate.lane_centering_controller._body_required)
self.assertIsNone(candidate.lane_centering_controller._body)
self.assertEqual(candidate.last_lane_centering_result.reason, 'unqualified_body')
self.assertEqual(candidate.last_lane_centering_result.correction, 0.)
self.assertIsNone(candidate.lane_centering_controller._tracker.path)
def test_shared_platform_requires_verified_body_without_retuning_geometry(self):
from openpilot.starpilot.lateral.lane_clearance import body_dimensions_required, interface_dimensions
stock, tagged = ioniq_candidate()
for cp in (stock, tagged):
before = cp.to_dict()
self.assertTrue(body_dimensions_required(HyundaiCarInterface, cp))
self.assertIsNone(interface_dimensions(HyundaiCarInterface, cp))
self.assertEqual(cp.to_dict(), before)
other = cp.as_reader().as_builder()
other.carFingerprint = HyundaiCAR.HYUNDAI_IONIQ_5
self.assertFalse(body_dimensions_required(HyundaiCarInterface, other))
self.assertIsNone(interface_dimensions(HyundaiCarInterface, other))
self.assertFalse(body_dimensions_required(HondaCarInterface, stock))
self.assertIsNone(interface_dimensions(HondaCarInterface, stock))
def test_shared_feed_is_one_current_batch_and_delay_qualifies_at_real_rate(self):
with OpenpilotPrefix(), mock.patch.dict(os.environ, {'REPLAY': '1', 'SIMULATION': '0'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
_, tagged = ioniq_candidate()
params.put('CarParams', tagged.to_bytes(), block=True)
controls = Controls()
now = 2_000_000_000
batches = []
with mock.patch.object(controls.sm, 'update_msgs', side_effect=lambda stamp, rows: batches.append((stamp, rows))):
feed(controls, now, 0)
self.assertEqual(len(batches), 1)
self.assertEqual(batches[0][0], now / 1e9)
delays = [row for row in batches[0][1] if row.which() == 'lateralDelay']
self.assertEqual([row.logMonoTime for row in delays], [now])
before = controls.sm.frame
controls.sm.update_msgs(*batches[0])
self.assertEqual(controls.sm.frame, before + 1)
self.assertEqual(controls.sm.freq_tracker['lateralDelay'].avg_dt.count, 0)
self.assertFalse(controls.sm.all_checks(['lateralDelay']))
for tick in range(1, 26):
before = controls.sm.frame
feed(controls, now + tick * 10_000_000, tick)
self.assertEqual(controls.sm.frame, before + 1)
if tick < 25:
self.assertFalse(controls.sm.all_checks(['lateralDelay']))
self.assertEqual(controls.sm.freq_tracker['lateralDelay'].avg_dt.get_average(), .25)
self.assertTrue(controls.sm.all_checks(['lateralDelay']))
for tick in range(26, 31):
feed(controls, now + tick * 10_000_000, tick, delay_fast=True)
self.assertFalse(controls.sm.all_checks(['lateralDelay']))
def test_body_guard_rejects_incorrect_delay_rate_with_lateral_axis_still_active(self):
with OpenpilotPrefix(), known_body_fixture(), mock.patch.dict(os.environ, {'LANE_CENTERING_REPLAY_RUNTIME': '0', 'REPLAY': '1',
'AOL_REPLAY_RUNTIME': '0'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
_, tagged = ioniq_candidate()
params.put('CarParams', tagged.to_bytes(), block=True)
params.put_bool('LaneCentering', True, block=True)
params.put('LaneCenteringE2EAuthority', 0., block=True)
controls = Controls(predict_lane_clearance=True)
for tick in range(35):
feed(controls, 2_000_000_000 + tick * 10_000_000, tick)
command, _ = controls.state_control()
self.assertTrue(command.latActive)
self.assertGreater(controls.last_lane_centering_result.correction, 0.)
for tick in range(35, 45):
feed(controls, 2_000_000_000 + tick * 10_000_000, tick, delay_fast=True)
command, _ = controls.state_control()
self.assertTrue(command.latActive)
self.assertFalse(controls.sm.all_checks(['lateralDelay']))
self.assertEqual(controls.last_lane_centering_result.correction, 0.)
self.assertEqual(controls.lane_centering_applied, 0.)
def test_clearance_delay_uses_original_header_and_rejects_stale_or_future_source(self):
from openpilot.starpilot.lateral.lane_clearance import current_delay
frame = SimpleNamespace(logMonoTime={'lateralDelay':1_000_000_000}, all_checks=lambda services:True)
class Source:
logMonoTime = frame.logMonoTime
all_checks = staticmethod(frame.all_checks)
def __getitem__(self, service):
return SimpleNamespace(lateralDelay=.1)
source = Source()
self.assertEqual(current_delay(source, 1_250_000_000), .1)
self.assertIsNone(current_delay(source, 1_626_000_000))
self.assertIsNone(current_delay(source, 999_999_999))
self.assertEqual(source.logMonoTime['lateralDelay'], 1_000_000_000)
def test_steering_pause_original_timing_source_loss_and_units(self):
from openpilot.starpilot.lateral.pause import LateralPause, PauseSettings, read_settings as read_pause
state = SimpleNamespace(vEgo=0., leftBlinker=True, rightBlinker=False, canValid=True, canTimeout=False)
pause = LateralPause(PauseSettings(10., True, .2))
now = 1_000_000_000
self.assertFalse(pause.allowed(state, now_ns=now, source_ns=now))
state.leftBlinker = False
self.assertFalse(pause.allowed(state, now_ns=now + 10_000_000, source_ns=now + 10_000_000))
self.assertFalse(pause.allowed(state, now_ns=now + 209_000_000, source_ns=now + 209_000_000))
self.assertTrue(pause.allowed(state, now_ns=now + 210_000_000, source_ns=now + 210_000_000))
# A short source/drive-observation gap cannot erase a committed delay.
pause = LateralPause(PauseSettings(10., True, .2))
state.leftBlinker = True
self.assertFalse(pause.allowed(state, now_ns=now, source_ns=now, drive_id=1))
state.leftBlinker = False
self.assertFalse(pause.allowed(state, now_ns=now + 10_000_000, source_ns=now + 10_000_000, drive_id=1))
self.assertFalse(pause.allowed(state, now_ns=now + 50_000_000, source_ns=0, drive_id=1))
self.assertFalse(pause.allowed(state, now_ns=now + 60_000_000, source_ns=now + 60_000_000, drive_id=0))
self.assertFalse(pause.allowed(state, now_ns=now + 100_000_000, source_ns=now + 100_000_000, drive_id=1))
self.assertTrue(pause.allowed(state, now_ns=now + 210_000_000, source_ns=now + 210_000_000, drive_id=1))
# If signal-off was hidden by the gap, begin the delay on its first fresh observation.
pause = LateralPause(PauseSettings(10., True, .2))
state.leftBlinker = True
pause.allowed(state, now_ns=now, source_ns=now, drive_id=1)
state.leftBlinker = False
self.assertFalse(pause.allowed(state, now_ns=now + 50_000_000, source_ns=0, drive_id=1))
self.assertFalse(pause.allowed(state, now_ns=now + 100_000_000, source_ns=now + 100_000_000, drive_id=1))
self.assertFalse(pause.allowed(state, now_ns=now + 299_000_000, source_ns=now + 299_000_000, drive_id=1))
self.assertTrue(pause.allowed(state, now_ns=now + 300_000_000, source_ns=now + 300_000_000, drive_id=1))
# The half-threshold condition is strict; equality never starts a delay.
state.leftBlinker, state.vEgo = True, 5.
pause.allowed(state, now_ns=now + 300_000_000, source_ns=now + 300_000_000)
state.leftBlinker = False
self.assertTrue(pause.allowed(state, now_ns=now + 310_000_000, source_ns=now + 310_000_000))
self.assertFalse(pause.allowed(state, now_ns=now + 700_000_000, source_ns=now + 310_000_000))
self.assertTrue(pause.allowed(state, now_ns=now + 710_000_000, source_ns=now + 710_000_000))
self.assertFalse(pause.allowed(state, now_ns=now, source_ns=now))
with OpenpilotPrefix():
params = Params()
params.put('PauseLateralSpeed', 20., block=True)
params.put_bool('IsMetric', False, block=True)
settings = read_pause(params)
assert settings is not None
self.assertAlmostEqual(settings.speed_mps, 20. * .44704)
params.put_bool('IsMetric', True, block=True)
settings = read_pause(params)
assert settings is not None
self.assertAlmostEqual(settings.speed_mps, 20. / 3.6)
Path(params.get_param_path('PauseLateralSpeed')).write_bytes(b'nan')
self.assertIsNone(read_pause(params))
def test_invalid_saved_pause_settings_preserve_committed_delay(self):
from openpilot.starpilot.lateral.pause import LateralPause, PauseSettings, read_settings as read_pause
with OpenpilotPrefix():
params = Params()
self.assertEqual(read_pause(params), PauseSettings())
params.put('PauseLateralSpeed', 20., block=True)
params.put_bool('PauseLateralOnSignal', True, block=True)
params.put('LateralResumeDelay', 5., block=True)
pause = LateralPause(read_pause(params), params)
expected = pause.settings
state = SimpleNamespace(vEgo=0., leftBlinker=True, rightBlinker=False, canValid=True, canTimeout=False)
now = 1_000_000_000
self.assertFalse(pause.allowed(state, now_ns=now, source_ns=now))
state.leftBlinker = False
self.assertFalse(pause.allowed(state, now_ns=now + 10_000_000, source_ns=now + 10_000_000))
path = Path(params.get_param_path('PauseLateralSpeed'))
for tick, raw in ((2, b'nan'), (3, b'')):
path.write_bytes(raw)
self.assertIsNone(read_pause(params))
self.assertFalse(pause.allowed(state, now_ns=tick * now, source_ns=tick * now))
self.assertEqual(pause.settings, expected)
path.unlink()
path.mkdir() # A nonregular Params object is unreadable, not an explicit zero.
self.assertIsNone(read_pause(params))
self.assertFalse(pause.allowed(state, now_ns=4 * now, source_ns=4 * now))
self.assertEqual(pause.settings, expected)
path.rmdir()
params.put('PauseLateralSpeed', 20., block=True)
self.assertFalse(pause.allowed(state, now_ns=5 * now, source_ns=5 * now))
self.assertTrue(pause.allowed(state, now_ns=6_010_000_000, source_ns=6_010_000_000))
# Only a valid explicit opt-out (or canonical absence) disables the policy.
state.leftBlinker = True
self.assertFalse(pause.allowed(state, now_ns=7 * now, source_ns=7 * now))
params.put('PauseLateralSpeed', 0., block=True)
self.assertTrue(pause.allowed(state, now_ns=8 * now, source_ns=8 * now))
self.assertEqual(pause.settings.speed_mps, 0.)
path.write_bytes(b'nan')
self.assertEqual(LateralPause(read_pause(params)).settings, PauseSettings())
def test_actual_controls_steering_pause_angle_standard_and_torque_aol(self):
from opendbc.car.ford.values import CAR as FORD
with OpenpilotPrefix(), mock.patch.dict(os.environ, {'REPLAY': '1', 'LANE_CENTERING_REPLAY_RUNTIME': '0'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
params.put('PauseLateralSpeed', 10., block=True)
params.put_bool('PauseLateralOnSignal', True, block=True)
params.put('LateralResumeDelay', .1, block=True)
params.put_bool('IsMetric', False, block=True)
for marked in (False, True):
with self.subTest(marked=marked):
if marked:
cp = publish_corolla_params(params)
else:
cp = interfaces[FORD.FORD_ESCAPE_MK4].get_non_essential_params(FORD.FORD_ESCAPE_MK4)
params.put('CarParams', cp.to_bytes(), block=True)
controls = Controls()
self.assertEqual(controls.CP.lateralTuning.which(), 'torque' if marked else 'pid')
for tick in range(25):
now = 2_000_000_000 + tick * 10_000_000
feed(controls, now, tick, signal=1 <= tick <= 5, speed=2., fault=tick == 20,
native_allowed=tick != 21, independent=marked, active=not marked, enabled=not marked)
if tick == 22:
controls.sm.valid['carState'] = False
command, _ = controls.state_control()
expected = tick == 0 or tick >= 16 and tick not in (20, 22) and (not marked or tick != 21)
self.assertEqual(command.latActive, expected, (marked, tick))
self.assertEqual(command.enabled, not marked)
# A new drive discards the prior low-speed turn's delay.
feed(controls, 2_250_000_000, 25, signal=True, speed=0.)
self.assertFalse(controls.state_control()[0].latActive)
feed(controls, 2_260_000_000, 26, speed=2., drive_id=2_255_000_000)
self.assertTrue(controls.state_control()[0].latActive)
# Live explicit opt-out refreshes without changing intent or the saved choice.
params.put('PauseLateralSpeed', 0., block=True)
feed(controls, 3_300_000_000, 30, signal=True, speed=2.)
self.assertTrue(controls.state_control()[0].latActive)
self.assertEqual(params.get('PauseLateralSpeed'), 0.)
params.put('PauseLateralSpeed', 10., block=True)
def test_toyota_aol_remains_active_at_standstill_with_current_native_permission(self):
with OpenpilotPrefix(), mock.patch.dict(os.environ, {'REPLAY': '1'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
cp = publish_corolla_params(params)
params.put('CarParams', cp.to_bytes(), block=True)
controls = Controls()
self.assertEqual(cp.minSteerSpeed, 0.)
for tick, (speed, allowed, fault) in enumerate(((2., True, False), (0., True, False),
(0., False, False), (0., True, True),
(0., True, False), (2., True, False))):
feed(controls, 2_000_000_000 + tick * 10_000_000, tick, speed=speed,
active=False, enabled=False, independent=True, native_allowed=allowed, fault=fault)
command, _ = controls.state_control()
self.assertEqual(command.latActive, allowed and not fault, (tick, speed))
self.assertFalse(command.longActive)
self.assertFalse(command.enabled)
def test_strength_saved_values_and_live_refresh_keep_existing_admission(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
params = SimpleNamespace(get_param_path=lambda key: str(root / key))
(root / 'LaneCentering').write_bytes(b'1')
self.assertEqual(read_settings(params).strength, 1.5)
(root / 'LaneCenteringStrength').write_bytes(b'1.0')
self.assertEqual(read_settings(params).strength, 1.0)
self.assertEqual((root / 'LaneCenteringStrength').read_bytes(), b'1.0')
(root / 'LaneCenteringStrength').unlink()
host = LaneCenteringHost(params)
now = 10_000_000_000
first = host.sample(Frame(now), now_ns=now, lateral_active=True, longitudinal_active=False)
self.assertEqual(first.settings.strength, 1.5)
(root / 'LaneCenteringStrength').write_bytes(b'3.0')
now += host.REFRESH_NS
updated = host.sample(Frame(now), now_ns=now, lateral_active=True, longitudinal_active=False)
self.assertEqual(updated.settings.strength, 3.0)
restarted = LaneCenteringHost(params).sample(Frame(now), now_ns=now, lateral_active=True, longitudinal_active=False)
self.assertEqual(restarted.settings.strength, 3.0)
self.assertEqual((root / 'LaneCenteringStrength').read_bytes(), b'3.0')
self.assertTrue(updated.time_discontinuity)
for invalid in (b'nan', b'0.49', b'3.001', b'true'):
(root / 'LaneCenteringStrength').write_bytes(invalid)
self.assertFalse(read_settings(params).enabled)
now += host.REFRESH_NS
self.assertIsNone(host.sample(Frame(now), now_ns=now, lateral_active=True, longitudinal_active=False))
def test_normal_reader_admission_uses_steering_and_saved_preference_gates(self):
with OpenpilotPrefix(), mock.patch.dict(os.environ, {'LANE_CENTERING_REPLAY_RUNTIME': '0'}):
params = Params()
for alternate in (False, True):
with self.subTest(alternate=alternate):
stock, tagged = ioniq_candidate(alternate=alternate)
self.assertTrue(runtime_supported(stock))
self.assertTrue(runtime_supported(tagged))
self.assertFalse(read_settings(params).enabled)
params.put_bool('LaneCentering', True, block=True)
self.assertTrue(read_settings(params).enabled)
self.assertTrue(runtime_supported(interfaces[CAR.TOYOTA_COROLLA_TSS2].get_non_essential_params(CAR.TOYOTA_COROLLA_TSS2)))
for safety_model in ('noOutput', 'silent', 'allOutput'):
tagged.safetyConfigs[0].safetyModel = safety_model
self.assertFalse(runtime_supported(tagged))
params.put_bool('LaneCentering', False, block=True)
_, tagged = ioniq_candidate()
params.put_bool('LaneCentering', True, block=True)
Path(params.get_param_path('LaneCenterOffset')).write_bytes(b'nan')
self.assertFalse(read_settings(params).enabled)
with mock.patch.dict(os.environ, {'LANE_CENTERING_REPLAY_RUNTIME': '1'}):
self.assertTrue(runtime_supported(tagged))
def test_actual_tagged_ioniq_saved_host_uses_fresh_lateral_authority(self):
with OpenpilotPrefix(), known_body_fixture(), mock.patch.dict(os.environ, {'LANE_CENTERING_REPLAY_RUNTIME': '0', 'REPLAY': '1',
'AOL_REPLAY_RUNTIME': '0'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
stock, tagged = ioniq_candidate()
params.put('LaneCenteringE2EAuthority', 0.0, block=True)
params.put('CarParams', tagged.to_bytes(), block=True)
disabled = Controls(predict_lane_clearance=True)
self.assertIsNotNone(disabled.lane_centering_host)
frame = Frame(1_000_000_000)
self.assertIsNone(disabled.lane_centering_host.sample(frame, now_ns=1_000_000_000,
lateral_active=True, longitudinal_active=False))
params.put_bool('LaneCentering', True, block=True)
controls = disabled
self.assertEqual(controls.lane_centering_controller._body.width, 1.88)
self.assertEqual(controls.lane_centering_controller._body.length, 4.855)
self.assertIsNotNone(controls.lane_centering_host)
for tick in range(35):
now = 2_000_000_000 + tick * 10_000_000
feed(controls, now, tick)
command, _ = controls.state_control()
self.assertTrue(command.latActive)
self.assertIsNotNone(controls.last_lane_centering_result)
self.assertGreater(controls.last_lane_centering_result.correction, 0.0)
feed(controls, now + 10_000_000, 35, model_age=200_000_000)
controls.state_control()
self.assertIsNone(controls.last_lane_centering_result)
params.put_bool('LaneCentering', False, block=True)
feed(controls, 3_000_000_000, 36)
controls.state_control()
self.assertIsNone(controls.last_lane_centering_result)
self.assertEqual(controls.lane_centering_applied, 0.0)
params.put('CarParams', stock.to_bytes(), block=True)
self.assertIsNotNone(Controls().lane_centering_host)
def test_tagged_ioniq_four_axes_and_stale_reset(self):
with OpenpilotPrefix(), known_body_fixture(), mock.patch.dict(os.environ, {'LANE_CENTERING_REPLAY_RUNTIME': '0', 'REPLAY': '1',
'AOL_REPLAY_RUNTIME': '0'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
_, tagged = ioniq_candidate()
params.put('CarParams', tagged.to_bytes(), block=True)
params.put_bool('LaneCentering', True, block=True)
params.put('LaneCenteringE2EAuthority', 0.0, block=True)
controls = Controls()
# This qualified-axis test also explicitly primes the 4 Hz delay source.
initial = messaging.new_message('lateralDelay', valid=True, logMonoTime=750_000_000)
initial.lateralDelay.lateralDelay = .1
controls.sm.update_msgs(.75, [initial.as_reader()])
self.assertIsNotNone(controls.lane_centering_host)
# Inject already-qualified independent-axis results at the consumer seam;
# AOL's native/session checks are covered by its own wire tests.
controls.aol_replay = True
tick = 0
for lat, lon in ((True, True), (True, False), (False, True), (False, False)):
with self.subTest(lat=lat, lon=lon), \
mock.patch('openpilot.selfdrive.controls.controlsd.current_axis', return_value=SimpleNamespace(
sessionId='test-session', nativeAcknowledged=True, lateralActive=lat, desiredLateral=lat,
longitudinalActive=lon, desiredLongitudinal=lon)), \
mock.patch('openpilot.selfdrive.controls.controlsd.current_native', return_value=SimpleNamespace(
requestedLateral=lat, lateralAllowed=lat, requestedLongitudinal=lon, longitudinalAllowed=lon)):
for _ in range(10):
now = 1_000_000_000 + tick * 10_000_000
feed(controls, now, tick, active=lat, enabled=lon)
command, _ = controls.state_control()
tick += 1
self.assertEqual((command.latActive, command.longActive), (lat, lon))
self.assertIsNotNone(controls.last_lane_centering_result)
self.assertEqual(controls.last_lane_centering_result.correction > 0.0, lat)
with mock.patch('openpilot.selfdrive.controls.controlsd.current_axis', return_value=SimpleNamespace(
sessionId='test-session', nativeAcknowledged=True, lateralActive=True, desiredLateral=True,
longitudinalActive=False, desiredLongitudinal=False)), \
mock.patch('openpilot.selfdrive.controls.controlsd.current_native', return_value=SimpleNamespace(
requestedLateral=True, lateralAllowed=True, requestedLongitudinal=False, longitudinalAllowed=False)):
now = 1_000_000_000 + tick * 10_000_000
feed(controls, now, tick, active=False, enabled=False, model_age=200_000_000)
command, _ = controls.state_control()
self.assertTrue(command.latActive)
self.assertFalse(command.longActive)
self.assertIsNone(controls.last_lane_centering_result)
for recovery_tick in range(tick + 1, tick + 6):
now = 1_000_000_000 + recovery_tick * 10_000_000
feed(controls, now, recovery_tick, active=False, enabled=False)
controls.state_control()
self.assertGreater(controls.last_lane_centering_result.correction, 0.0)
def test_host_refusal_classification_preserves_admission_and_expires(self):
with OpenpilotPrefix():
params = Params()
params.put_bool('LaneCentering', True, block=True)
host = LaneCenteringHost(params)
now = 1_000_000_000
def sample(frame, at=now):
return host.sample(frame, now_ns=at, lateral_active=True, longitudinal_active=True)
def recovered():
self.assertIsNotNone(sample(Frame(now)))
self.assertEqual(host.last_refusal, 'inactive')
recovered()
# Use the actual subscriber methods, including their ignore policy.
sm = messaging.SubMaster(list(host.REQUIRED), frequency=100)
for service in host.REQUIRED:
sm.alive[service] = sm.freq_ok[service] = sm.valid[service] = True
sm.logMonoTime[service] = now
car_state = messaging.new_message('carState', valid=True)
car_state.carState.canValid = True
sm.data['carState'] = car_state.as_reader().carState
for flag, reason in (('alive', 'not_alive'), ('freq_ok', 'frequency'), ('valid', 'invalid')):
for service in host.REQUIRED:
if flag == 'freq_ok' and service == 'selfdriveState':
continue
with self.subTest(flag=flag, service=service):
getattr(sm, flag)[service] = False
self.assertIsNone(sample(sm))
self.assertEqual(host.last_refusal, f'host_{service}_{reason}')
getattr(sm, flag)[service] = True
self.assertIsNotNone(sample(sm))
self.assertEqual(host.last_refusal, 'inactive')
for ignore, flag in (('ignore_alive', 'alive'), ('ignore_average_freq', 'freq_ok'), ('ignore_valid', 'valid')):
getattr(sm, ignore).append('modelV2')
getattr(sm, flag)['modelV2'] = False
self.assertIsNotNone(sample(sm))
self.assertEqual(host.last_refusal, 'inactive')
getattr(sm, flag)['modelV2'] = True
getattr(sm, ignore).remove('modelV2')
for service in host.REQUIRED:
for stamp, reason in ((0, 'missing_time'), (now + 1, 'future'), (now - 200_000_000, 'stale')):
frame = Frame(now)
frame.logMonoTime[service] = stamp
self.assertIsNone(sample(frame))
self.assertEqual(host.last_refusal, f'host_{service}_{reason}')
recovered()
frame = Frame(now)
frame.checked = False
self.assertIsNone(sample(frame))
self.assertEqual(host.last_refusal, 'host_checks')
recovered()
for valid, timeout, reason in ((False, False, 'host_can_invalid'), (True, True, 'host_can_timeout')):
frame = Frame(now)
frame.state.canValid, frame.state.canTimeout = valid, timeout
self.assertIsNone(sample(frame))
self.assertEqual(host.last_refusal, reason)
recovered()
self.assertIsNone(sample(Frame(0), 0))
self.assertEqual(host.last_refusal, 'host_clock')
recovered()
params.put_bool('LaneCentering', False, block=True)
self.assertIsNone(sample(Frame(now * 2), now * 2))
self.assertEqual(host.last_refusal, 'host_disabled')
def test_actual_controls_publishes_host_refusal_and_clears_it_on_recovery(self):
with OpenpilotPrefix(), mock.patch.dict(os.environ, {'AOL_REPLAY_RUNTIME': '0', 'REPLAY': '1'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
publish_corolla_params(params)
params.put_bool('LaneCentering', True, block=True)
params.put('LaneCenteringE2EAuthority', 0., block=True)
controls = Controls()
tick = 0
for tick in range(60):
feed(controls, 1_000_000_000 + tick * 10_000_000, tick)
controls.state_control()
for flag, reason in (('alive', 'not_alive'), ('freq_ok', 'frequency'), ('valid', 'invalid')):
for service in LaneCenteringHost.REQUIRED:
if flag == 'freq_ok' and service == 'selfdriveState':
continue
tick += 1
feed(controls, 1_000_000_000 + tick * 10_000_000, tick)
getattr(controls.sm, flag)[service] = False
command, lateral_log = controls.state_control()
self.assertIsNone(controls.last_lane_centering_result)
self.assertEqual(controls.lane_centering_applied, 0.)
controls.pm.send.reset_mock()
controls.publish(command, lateral_log)
feedback = [call.args[1] for call in controls.pm.send.call_args_list if call.args[0] == 'starpilotLateralState'][-1]
state = feedback.starpilotLateralState.laneCentering
self.assertEqual(state.reason, f'host_{service}_{reason}')
self.assertEqual((state.requestedCorrection, state.appliedCorrection), (0., 0.))
getattr(controls.sm, flag)[service] = True
tick += 1
feed(controls, 1_000_000_000 + tick * 10_000_000, tick)
command, lateral_log = controls.state_control()
self.assertIsNotNone(controls.last_lane_centering_result)
self.assertEqual(controls.lane_centering_inactive_reason, 'inactive')
controls.pm.send.reset_mock()
controls.publish(command, lateral_log)
feedback = [call.args[1] for call in controls.pm.send.call_args_list if call.args[0] == 'starpilotLateralState'][-1]
self.assertEqual(feedback.starpilotLateralState.laneCentering.reason, controls.last_lane_centering_result.reason)
def test_actual_controls_lane_change_has_zero_added_and_retained_contribution(self):
from openpilot.cereal import log
for state in (log.LaneChangeState.preLaneChange, log.LaneChangeState.laneChangeStarting,
log.LaneChangeState.laneChangeFinishing):
for signal in (False, True):
for pause in (False, True):
with self.subTest(state=state, signal=signal, pause=pause), OpenpilotPrefix(), \
mock.patch.dict(os.environ, {'AOL_REPLAY_RUNTIME': '0', 'REPLAY': '1'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
publish_corolla_params(params)
params.put_bool('LaneCentering', True, block=True)
params.put_bool('LaneCenteringPauseOnSignal', pause, block=True)
params.put('LaneCenteringE2EAuthority', 0., block=True)
reference, candidate = Controls(), Controls()
for tick in range(60):
now = 1_000_000_000 + tick * 10_000_000
for controls in (reference, candidate):
feed(controls, now, tick)
controls.state_control()
self.assertGreater(candidate.last_lane_centering_result.correction, 0.)
self.assertEqual(candidate.desired_curvature, reference.desired_curvature)
# Both native controllers have the same complete centering history.
reference.lane_centering_host = None
changed_model = model()
changed_model.meta.laneChangeState = state
with mock.patch(f'{__name__}.model', return_value=changed_model):
for tick in range(60, 70):
now = 1_000_000_000 + tick * 10_000_000
feed(reference, now, tick, signal=signal)
feed(candidate, now, tick, signal=signal)
nominal, _ = reference.state_control()
command, _ = candidate.state_control()
self.assertEqual(command.to_dict(), nominal.to_dict())
self.assertEqual(candidate.desired_curvature, reference.desired_curvature)
self.assertEqual(candidate.last_lane_centering_result.reason, 'lane_change')
self.assertEqual(candidate.last_lane_centering_result.correction, 0.)
self.assertEqual(candidate.lane_centering_applied, 0.)
self.assertEqual(candidate.lane_centering_controller._correction, 0.)
self.assertIsNone(candidate.lane_centering_controller._tracker.path)
def test_fresh_polled_state_with_slow_consumer_keeps_actual_centering(self):
with OpenpilotPrefix(), mock.patch.dict(os.environ, {'AOL_REPLAY_RUNTIME': '0', 'REPLAY': '1'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
publish_corolla_params(params)
params.put_bool('LaneCentering', True, block=True)
params.put('LaneCenteringE2EAuthority', 0., block=True)
reference, candidate = Controls(), Controls()
for tick in range(100):
now = 1_000_000_000 + tick * 10_000_000
feed(reference, now, tick)
feed(candidate, now, tick)
if tick >= 30:
candidate.sm.freq_ok['selfdriveState'] = False
nominal, _ = reference.state_control()
command, _ = candidate.state_control()
self.assertEqual(command.to_dict(), nominal.to_dict())
if tick >= 30:
self.assertIsNotNone(candidate.last_lane_centering_result)
self.assertGreater(candidate.lane_centering_applied, 0.)
for age in (25_000_001, -1):
tick += 1
now = 1_000_000_000 + tick * 10_000_000
feed(candidate, now, tick)
candidate.sm.freq_ok['selfdriveState'] = False
candidate.sm.logMonoTime['selfdriveState'] = now - age
candidate.state_control()
self.assertIsNone(candidate.last_lane_centering_result)
self.assertEqual(candidate.lane_centering_applied, 0.)
self.assertEqual(candidate.lane_centering_controller._correction, 0.)
for flag in ('alive', 'valid'):
tick += 1
now = 1_000_000_000 + tick * 10_000_000
feed(candidate, now, tick)
candidate.sm.freq_ok['selfdriveState'] = False
getattr(candidate.sm, flag)['selfdriveState'] = False
candidate.state_control()
self.assertIsNone(candidate.last_lane_centering_result)
self.assertEqual(candidate.lane_centering_applied, 0.)
getattr(candidate.sm, flag)['selfdriveState'] = True
def test_actual_single_line_cold_start_moves_from_biased_path_and_clears_for_lane_change(self):
from openpilot.starpilot.lateral.tests.test_lane_centering import single
for missing_right, sign in ((True, 1), (False, -1)):
with self.subTest(missing_right=missing_right), OpenpilotPrefix(), \
mock.patch.dict(os.environ, {'AOL_REPLAY_RUNTIME': '0', 'REPLAY': '1'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
publish_corolla_params(params)
params.put_bool('LaneCentering', True, block=True)
params.put('LaneCenteringE2EAuthority', 0., block=True)
candidate = Controls()
m = single(missing_right=missing_right, line=-1.5, edge=10.)
with mock.patch(f'{__name__}.model', return_value=m):
for tick in range(100):
feed(candidate, 1_000_000_000 + tick * 10_000_000, tick)
candidate.state_control()
self.assertGreater(sign * candidate.last_lane_centering_result.correction, 0.)
self.assertGreater(sign * candidate.lane_centering_applied, 0.)
self.assertIsNone(candidate.lane_centering_controller._tracker.width)
assert candidate.lane_centering_controller._tracker.spacing is not None
self.assertAlmostEqual(candidate.lane_centering_controller._tracker.spacing, 1.7)
m.meta.laneChangeState = 'laneChangeStarting'
feed(candidate, 2_000_000_000, 100)
candidate.state_control()
self.assertEqual(candidate.last_lane_centering_result.reason, 'lane_change')
self.assertEqual(candidate.lane_centering_applied, 0.)
self.assertEqual(candidate.lane_centering_controller._correction, 0.)
self.assertIsNone(candidate.lane_centering_controller._tracker.path)
def test_ordinary_centering_and_explicit_prediction_on_close_observed_edge(self):
from opendbc.car.hyundai.tests.test_lane_body import identity
with OpenpilotPrefix(), mock.patch.dict(os.environ, {'LANE_CENTERING_REPLAY_RUNTIME': '0', 'REPLAY': '1',
'AOL_REPLAY_RUNTIME': '0'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
_, cp = ioniq_candidate()
metadata = identity()
cp.carVin, cp.carFw = metadata.carVin, [vars(fw) for fw in metadata.carFw]
params.put('CarParams', cp.to_bytes(), block=True)
params.put_bool('LaneCentering', True, block=True)
params.put('LaneCenteringE2EAuthority', 0., block=True)
ordinary = Controls()
m = model(left=-1.2, right=1.8)
m.init('roadEdges', 2)
for edge, side in zip(m.roadEdges, (-1., 1.), strict=True):
edge.x, edge.y = list(range(51)), [2.5 * side] * 51
m.roadEdgeStds = [.17, 1.]
with mock.patch(f'{__name__}.model', return_value=m):
for tick in range(80):
feed(ordinary, 1_000_000_000 + tick * 10_000_000, tick)
ordinary.state_control()
self.assertGreater(ordinary.lane_centering_applied, 0.)
self.assertIsNone(ordinary.lane_centering_controller._body)
predicted = Controls(predict_lane_clearance=True)
self.assertIsNotNone(predicted.lane_centering_controller._body)
with mock.patch(f'{__name__}.model', return_value=m):
for tick in range(80):
feed(predicted, 1_000_000_000 + tick * 10_000_000, tick)
predicted.state_control()
self.assertEqual(predicted.last_lane_centering_result.reason, 'clearance_limited')
self.assertEqual(predicted.lane_centering_applied, 0.)
self.assertEqual(predicted.CP.to_dict(), ordinary.CP.to_dict())
def test_saved_settings_bounds_and_corruption(self):
with OpenpilotPrefix():
params = Params()
self.assertFalse(read_settings(params).enabled)
self.assertTrue(read_settings(params).pause_on_signal)
params.put_bool('LaneCentering', True, block=True)
params.put('LaneCenterOffset', -0.2, block=True)
params.put('LaneCenteringE2EAuthority', 0.5, block=True)
settings = read_settings(params)
self.assertEqual((settings.enabled, settings.offset_m, settings.e2e_authority), (True, -0.2, 0.5))
for key, values in (('LaneCenterOffset', (b'bad', b'nan', b'inf', b'0.31')),
('LaneCenteringE2EAuthority', (b'', b'1.01', b'-0.1')),
('LaneCenteringPauseOnSignal', (b'false', b'2'))):
path = Path(params.get_param_path(key))
original = path.read_bytes() if path.exists() else None
for value in values:
with self.subTest(key=key, value=value):
path.write_bytes(value)
self.assertFalse(read_settings(params).enabled)
self.assertTrue(params.get_bool('LaneCentering'))
if original is None:
path.unlink()
else:
path.write_bytes(original)
def test_authority_is_derived_from_actual_axes(self):
with OpenpilotPrefix():
params = Params()
params.put_bool('LaneCentering', True, block=True)
host = LaneCenteringHost(params)
for tick, (lat, lon, mode) in enumerate(((False, False, ControlMode.OFF), (False, True, ControlMode.LONGITUDINAL_ONLY),
(True, False, ControlMode.LATERAL_ONLY), (True, True, ControlMode.COMBINED))):
now = 1_000_000_000 + tick * 10_000_000
request = host.sample(Frame(now), now_ns=now, lateral_active=lat, longitudinal_active=lon)
self.assertIsNotNone(request)
self.assertEqual(request.mode, mode)
def test_stale_future_invalid_input_and_clock_gap(self):
with OpenpilotPrefix():
params = Params()
params.put_bool('LaneCentering', True, block=True)
host = LaneCenteringHost(params)
now = 1_000_000_000
def sample(frame, at=now):
return host.sample(frame, now_ns=at, lateral_active=True, longitudinal_active=True)
self.assertIsNotNone(sample(Frame(now)))
for service in host.REQUIRED:
for age in (200_000_000, -1):
frame = Frame(now)
frame.logMonoTime[service] = now - age
self.assertIsNone(sample(frame))
frame = Frame(now)
frame.checked = False
self.assertIsNone(sample(frame))
frame.checked, frame.state.canValid = True, False
self.assertIsNone(sample(frame))
frame.state.canValid, frame.state.canTimeout = True, True
self.assertIsNone(sample(frame))
frame.state.canTimeout = False
frame.logMonoTime['modelV2'] = 0
self.assertIsNone(sample(frame))
self.assertIsNone(sample(Frame(0), 0))
gap = sample(Frame(now + 100_000_000), now + 100_000_000)
self.assertTrue(gap.time_discontinuity)
self.assertFalse(sample(Frame(now + 110_000_000), now + 110_000_000).time_discontinuity)
self.assertTrue(sample(Frame(now), now).time_discontinuity)
def test_live_settings_refresh_and_corrupt_setting_disables(self):
with OpenpilotPrefix():
params = Params()
params.put_bool('LaneCentering', True, block=True)
host = LaneCenteringHost(params)
for now in (1_000_000_000, 1_010_000_000):
self.assertIsNotNone(host.sample(Frame(now), now_ns=now, lateral_active=True, longitudinal_active=False))
Path(params.get_param_path('LaneCenterOffset')).write_bytes(b'bad')
now = 2_000_000_000
self.assertIsNone(host.sample(Frame(now), now_ns=now, lateral_active=True, longitudinal_active=False))
self.assertTrue(params.get_bool('LaneCentering'))
def test_actual_controls_saved_settings_and_permission_gates(self):
scenarios = ('normal', 'disabled_setting', 'off', 'long_only', 'fault', 'override', 'signal', 'stale', 'invalid_can', 'can_timeout')
with OpenpilotPrefix(), mock.patch.dict(os.environ, {'AOL_REPLAY_RUNTIME': '0', 'REPLAY': '1'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
publish_corolla_params(params)
for scenario in scenarios:
with self.subTest(scenario=scenario):
params.put_bool('LaneCentering', scenario != 'disabled_setting', block=True)
params.put('LaneCenteringE2EAuthority', 0.0, block=True)
with mock.patch.dict(os.environ, {'LANE_CENTERING_REPLAY_RUNTIME': '0'}):
baseline = Controls()
with mock.patch.dict(os.environ, {'LANE_CENTERING_REPLAY_RUNTIME': '1'}):
candidate = Controls()
self.assertIsNotNone(baseline.lane_centering_host)
baseline.lane_centering_host = None # Explicit no-feature reference; Toyota now uses the normal host.
previous = 0.0
for tick in range(120):
now = 1_000_000_000 + tick * 10_000_000
phase = scenario if tick >= 80 else 'normal'
options = {'active': phase not in ('off', 'long_only'), 'enabled': phase != 'off',
'fault': phase == 'fault', 'override': phase == 'override', 'signal': phase == 'signal',
'model_age': 200_000_000 if phase == 'stale' else tick % 5 * 10_000_000,
'can_valid': phase != 'invalid_can', 'can_timeout': phase == 'can_timeout'}
for controls in (baseline, candidate):
feed(controls, now, tick, **options)
reference, _ = baseline.state_control()
command, _ = candidate.state_control()
result = candidate.last_lane_centering_result
self.assertEqual(command.latActive, reference.latActive)
self.assertEqual(command.longActive, reference.longActive)
self.assertEqual(command.actuators.accel, reference.actuators.accel)
if scenario == 'disabled_setting':
self.assertEqual(command.to_dict(), reference.to_dict())
elif phase in ('off', 'long_only', 'fault', 'override'):
self.assertEqual(result.correction, 0.0)
elif phase in ('stale', 'invalid_can', 'can_timeout'):
self.assertIsNone(result)
elif phase == 'signal':
self.assertLess(result.correction, previous)
elif tick >= 20:
self.assertGreater(result.correction, previous)
previous = result.correction if result is not None else 0.0
def test_actual_aol_lateral_only_controls_without_longitudinal(self):
with OpenpilotPrefix(), mock.patch.dict(os.environ, {'AOL_REPLAY_RUNTIME': '1', 'LANE_CENTERING_REPLAY_RUNTIME': '1',
'REPLAY': '1'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
params.put_bool('LaneCentering', True, block=True)
cp = HondaCarInterface.get_params(HondaCAR.HONDA_CIVIC_BOSCH, gen_empty_fingerprint(), [], True, False, False)
cp.safetyConfigs[-1].safetyParam |= 0x20 # Card's qualified AOL bit in serialized CarParams.
params.put('CarParams', cp.to_bytes(), block=True)
controls = Controls()
self.assertTrue(controls.aol_replay)
self.assertIsNotNone(controls.lane_centering_host)
for tick in range(40):
now = 1_000_000_000 + tick * 10_000_000
feed(controls, now, tick, active=False, enabled=False, aol_lat_only=True)
command, _ = controls.state_control()
self.assertFalse(command.enabled)
self.assertTrue(command.latActive)
self.assertFalse(command.longActive)
self.assertEqual(command.actuators.accel, 0.0)
assert controls.last_lane_centering_result is not None
self.assertGreater(controls.last_lane_centering_result.correction, 0.0)
previous = controls.last_lane_centering_result.correction
now += 10_000_000
feed(controls, now, 40, active=False, enabled=False, aol_lat_only=True, native_session='old-drive')
command, _ = controls.state_control()
self.assertFalse(command.latActive or command.longActive)
self.assertEqual(controls.last_lane_centering_result.correction, 0.0)
now += 10_000_000
feed(controls, now, 41, active=False, enabled=False, aol_lat_only=True)
command, _ = controls.state_control()
self.assertTrue(command.latActive)
self.assertFalse(command.longActive)
self.assertLess(controls.last_lane_centering_result.correction, previous)
for tick in range(42, 48):
now += 10_000_000
feed(controls, now, tick, active=False, enabled=False)
command, _ = controls.state_control()
self.assertFalse(command.latActive or command.longActive)
self.assertEqual(controls.last_lane_centering_result.correction, 0.0)
def test_stock_acc_lateral_stays_separate_from_aol(self):
with OpenpilotPrefix(), mock.patch.dict(os.environ, {'AOL_REPLAY_RUNTIME': '0', 'LANE_CENTERING_REPLAY_RUNTIME': '1',
'REPLAY': '1'}), \
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
params = Params()
params.put_bool('LaneCentering', True, block=True)
cp = interfaces[CAR.TOYOTA_COROLLA_TSS2].get_non_essential_params(CAR.TOYOTA_COROLLA_TSS2)
cp.openpilotLongitudinalControl = False
cp.pcmCruise = True
params.put('CarParams', cp.to_bytes(), block=True)
controls = Controls()
self.assertFalse(controls.aol_replay)
for tick in range(40):
now = 1_000_000_000 + tick * 10_000_000
feed(controls, now, tick)
command, _ = controls.state_control()
self.assertTrue(command.latActive)
self.assertFalse(command.longActive)
assert controls.last_lane_centering_result is not None
self.assertGreater(controls.last_lane_centering_result.correction, 0.0)