Files
StarPilot/openpilot/starpilot/aol/tests/test_runtime.py
T
dirwin31 be8cf95de8 AOL: Skip the temporary steering warning while the driver steers
A hard override on the Civic puts the EPS in NO_TORQUE_ALERT_1, which
reads as a temporary steering fault. The armed-AOL warning added
steerTempUnavailableSilent for it every time, even though the driver was
holding the wheel. CarSpecificEvents already stays quiet in that case.
Route 00000060: all 23 status-2 windows cleared before the driver let go.
The warning still shows if the fault is present while the wheel is free.

(cherry picked from commit f9167e2a24007c0822b4eae8870a24100a709325)
2026-10-26 08:25:28 -07:00

2193 lines
117 KiB
Python

import time
import unittest
from unittest import mock
from types import SimpleNamespace
from pathlib import Path
from openpilot.cereal import log, messaging
from openpilot.common.prefix import OpenpilotPrefix
from opendbc.car.structs import car
from openpilot.selfdrive.car.cruise import CRUISE_LONG_PRESS
from openpilot.common.params import Params
from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD
from openpilot.selfdrive.selfdrived.alertmanager import AlertManager
from openpilot.selfdrive.selfdrived.events import Events, ET
from openpilot.selfdrive.selfdrived.state import StateMachine
from openpilot.starpilot.aol.intent import AolCardIntent, AolSettings, disarming_fault, read_settings
from openpilot.starpilot.aol.runtime import AxisDecision, current_intent, current_native, decide_axes
from openpilot.starpilot.aol.tests import test_ioniq_availability as availability_fixture
from openpilot.starpilot.aol.tests.test_ioniq_availability import car_state
from openpilot.starpilot.aol.wire import IntentState, SafetyState, encode_intent, encode_safety
def empty_carstate_queue(sock):
original_receive = messaging.recv_one_or_none
def receive(received_sock):
return None if received_sock is sock else original_receive(received_sock)
return mock.patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one_or_none', side_effect=receive)
def sample_car_state(sd, state, stamp):
event = messaging.new_message('carState', valid=bool(state.canValid), logMonoTime=stamp)
event.carState = state
sock = object()
with mock.patch.object(sd, 'car_state_sock', sock, create=True), \
empty_carstate_queue(sock), \
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one', return_value=event.as_reader()), \
mock.patch.object(sd.sm, 'update', create=True):
return SelfdriveD.data_sample(sd)
class CardIntentTests(unittest.TestCase):
def test_optional_cancel_preserves_only_current_owned_lateral(self):
from opendbc.car.gm.tests.test_bolt_volt_configurations import ordinary_params
from opendbc.car.gm.values import CAR as GM
from openpilot.starpilot.aol.vehicle import policy_for
from openpilot.starpilot.nostalgia import aol_no_entry
from openpilot.starpilot.tests.test_ev6_startup import params
cp = params(alpha=True, radar=True)
policy = policy_for(cp)
cp.safetyConfigs[0].safetyParam |= policy.safety_param_addition
cp.alternativeExperience |= policy.alternative_experience_addition
self.assertEqual((cp.safetyConfigs[0].safetyParam, cp.alternativeExperience), (0x815, 32))
gm = ordinary_params(GM.CHEVROLET_VOLT_ASCM, alpha=True, sascm=True, radar=True)
now = 4_000_000_000
state = car_state()
state.cruiseState.available = True
button = car.CarState.ButtonEvent.Type.cancel
cancel = log.OnroadEvent.EventName.buttonCancel
class SM(dict):
model_ready = True
def all_checks(self, services=None):
return self.model_ready or services is None or 'modelV2' not in services
sm = SM(pandaStates=[], driverMonitoringState=SimpleNamespace(alertLevel=0, lockout=False, alwaysOnLockout=False),
extrinsicsCalibration=SimpleNamespace(calStatus=log.ExtrinsicsCalibration.Status.calibrated))
services = ('aolSafetyWire', 'aolIntentWire')
sm.valid = dict.fromkeys(services, True)
sm.alive = dict.fromkeys(services, True)
sm.seen = dict.fromkeys(services, True)
sm.logMonoTime = dict.fromkeys(services, now)
sd = SelfdriveD.__new__(SelfdriveD)
sd.CP = cp.as_reader()
sd.initialized, sd.enabled, sd.active = True, True, True
sd.aol_replay, sd.axis_transport_required = True, True
sd.aol_car_state_log_ns, sd.aol_session_id = now, 'cancel-session'
sd.aol_axis_decision = AxisDecision()
sd.aol_dm_lateral_inhibit, sd.nostalgia_paddle_cancel = False, False
sd.aol_authority_lost = False
sd.aol_settings = None
sd.events, sd.state_machine = Events(), StateMachine()
sd.state_machine.state = log.SelfdriveState.OpenpilotState.enabled
def step(*, pressed=True, case=None, extra=None):
sd.aol_dm_lateral_inhibit = False
state.buttonEvents = [] if pressed is None else [car.CarState.ButtonEvent(type=button, pressed=pressed)]
sd.events.clear()
if pressed is not None:
sd.events.add(cancel)
if extra is not None:
sd.events.add(extra)
sm.seen['aolIntentWire'] = case != 'missing'
sm.seen['aolSafetyWire'] = case != 'native_missing'
sm.model_ready = case != 'model'
sm['driverMonitoringState'].lockout = case == 'dm'
state.canValid = case != 'invalid_rx'
sm['aolIntentWire'] = encode_intent(IntentState('card', 1, now,
now - 30_000_001 if case == 'stale' else now, now + 200_000_000,
case != 'false_latch', False, False, case != 'unqualified', True, case != 'optional_off'))
if case == 'malformed':
sm['aolIntentWire'] = b'malformed'
sm['aolSafetyWire'] = encode_safety(SafetyState(1, True, now, now + 200_000_000,
int(sd.CP.safetyConfigs[0].safetyModel.raw), int(sd.CP.safetyConfigs[-1].safetyParam),
case != 'native_denied', False, case != 'native_mismatch', False, 'panda', 'cancel-session'))
sd.step()
return sd.aol_axis_decision
with mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=now), \
mock.patch.object(sd, 'sm', sm, create=True), mock.patch.object(sd, 'data_sample', side_effect=lambda: sample_car_state(sd, state, now)), \
mock.patch.object(sd, 'update_events'), mock.patch.object(sd, 'update_alerts'), \
mock.patch.object(sd, 'update_conditional_mode'), mock.patch.object(sd, 'publish_selfdriveState'), mock.patch.object(sd, 'publish_axis_proof'), \
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.aol_no_entry', wraps=aol_no_entry) as veto:
for pressed in (True, False, None):
retained = step(pressed=pressed)
self.assertTrue(retained.desired_lateral and retained.lateral_active)
self.assertFalse(sd.enabled or sd.active or retained.desired_longitudinal or retained.longitudinal_active)
self.assertTrue(veto.call_args.kwargs['allow_lateral_cancel'])
if pressed is not None:
self.assertIn(cancel, sd.events.names)
self.assertTrue(aol_no_entry([cancel], state, paddle_only_cancel=False))
for case in ('missing', 'false_latch', 'optional_off', 'stale', 'unqualified', 'malformed',
'invalid_rx', 'model', 'dm', 'native_missing', 'native_denied', 'native_mismatch'):
with self.subTest(case=case):
sd.state_machine.state = log.SelfdriveState.OpenpilotState.enabled
blocked = step(case=case)
self.assertFalse(blocked.lateral_active or blocked.longitudinal_active or sd.enabled or sd.active)
if case in ('missing', 'false_latch', 'optional_off', 'stale', 'unqualified', 'malformed'):
self.assertFalse(veto.call_args.kwargs['allow_lateral_cancel'])
for extra in (log.OnroadEvent.EventName.doorOpen, log.OnroadEvent.EventName.calibrationInvalid,
log.OnroadEvent.EventName.canError):
with self.subTest(extra=extra):
self.assertFalse(step(extra=extra).lateral_active)
for optional in (False, True):
with self.subTest(gm_optional=optional):
sd.CP = gm.as_reader()
sd.state_machine.state = log.SelfdriveState.OpenpilotState.enabled
self.assertFalse(step(case=None if optional else 'optional_off').lateral_active)
self.assertFalse(veto.call_args.kwargs['allow_lateral_cancel'])
def test_volt_ascm_low_speed_continuation_and_engagement_gates(self):
# Route 14 crosses 5 kph while enabled; the stock no-entry event must
# still block a new longitudinal engagement, without dropping active axes.
from opendbc.car.gm.tests.test_bolt_volt_configurations import ordinary_params
from opendbc.car.gm.values import CAR
import os
import struct
from openpilot.starpilot.aol.tests.test_gm import TestGmAol
with OpenpilotPrefix(), mock.patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}):
settings = Params()
for key in ('AlwaysOnLateral', 'AlphaLongitudinalEnabled', 'VoltOnePedalMode', 'VoltSNG', 'GMAutoHold'):
settings.put_bool(key, True, block=True)
selected = TestGmAol.card(ordinary_params(CAR.CHEVROLET_VOLT_ASCM, alpha=True,
sascm=True, radar=True), settings)
try:
cp = selected.CP.as_reader()
self.assertEqual((cp.safetyConfigs[0].safetyParam, cp.alternativeExperience), (0xD114, 32))
finally:
selected.vehicle_startup.close()
self.assertEqual(cp.minEnableSpeed, struct.unpack('f', struct.pack('f', 5. / 3.6))[0])
from openpilot.starpilot.nostalgia import aol_no_entry
low_speed = [log.OnroadEvent.EventName.belowEngageSpeed]
self.assertTrue(aol_no_entry(low_speed, car_state(), paddle_only_cancel=False))
self.assertFalse(aol_no_entry(low_speed, car_state(), paddle_only_cancel=False, allow_below_engage_speed=True))
now = 4_000_000_000
state = car_state()
state.cruiseState.available = True
class SM(dict):
def all_checks(self, _services=None):
return True
sm = SM(pandaStates=[], driverMonitoringState=SimpleNamespace(alertLevel=0, lockout=False, alwaysOnLockout=False),
extrinsicsCalibration=SimpleNamespace(calStatus=log.ExtrinsicsCalibration.Status.calibrated))
services = ('aolSafetyWire', 'aolIntentWire')
sm.valid = sm.alive = sm.seen = dict.fromkeys(services, True)
sm.logMonoTime = dict.fromkeys(services, now)
sd = SelfdriveD.__new__(SelfdriveD)
sd.CP = cp
sd.initialized, sd.enabled, sd.active = True, False, False
sd.aol_replay, sd.axis_transport_required = True, True
sd.aol_car_state_log_ns, sd.aol_session_id = now, 'volt-session'
sd.aol_axis_decision = AxisDecision()
sd.aol_dm_lateral_inhibit, sd.nostalgia_paddle_cancel = False, False
sd.aol_authority_lost = False
sd.aol_settings = None
sd.events, sd.state_machine = Events(), StateMachine()
def step(speed, *, armed, long_requested, event=None):
state.vEgo = speed
sd.events.clear()
if speed < cp.minEnableSpeed:
sd.events.add(log.OnroadEvent.EventName.belowEngageSpeed)
if event is not None:
sd.events.add(event)
sm['aolIntentWire'] = encode_intent(IntentState('card', 1, now, now, now + 200_000_000,
armed, False, False, True, armed))
sm['aolSafetyWire'] = encode_safety(SafetyState(1, True, now, now + 200_000_000,
int(cp.safetyConfigs[0].safetyModel.raw), 0xD114, True, long_requested,
True, long_requested, 'panda', 'volt-session'))
sd.step()
return sd.aol_axis_decision
with mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=now), \
mock.patch.object(sd, 'sm', sm, create=True), \
mock.patch.object(sd, 'data_sample', side_effect=lambda: sample_car_state(sd, state, now)), \
mock.patch.object(sd, 'update_events'), mock.patch.object(sd, 'update_alerts'), \
mock.patch.object(sd, 'update_conditional_mode'), mock.patch.object(sd, 'publish_selfdriveState'), mock.patch.object(sd, 'publish_axis_proof'):
combined = step(1.40, armed=False, long_requested=True,
event=log.OnroadEvent.EventName.buttonEnable)
self.assertEqual(combined.mode, 'combined')
for speed in (1.385523796, 0., 1.395637870):
self.assertEqual(step(speed, armed=False, long_requested=True).mode, 'combined')
self.assertTrue(sd.enabled)
step(1.40, armed=True, long_requested=False, event=log.OnroadEvent.EventName.buttonCancel)
blocked = step(1.385523796, armed=True, long_requested=False,
event=log.OnroadEvent.EventName.buttonEnable)
self.assertTrue(sd.events.contains(ET.NO_ENTRY))
self.assertFalse(sd.enabled or blocked.desired_longitudinal)
self.assertEqual(blocked.mode, 'lateralOnly')
self.assertEqual(step(1.395637870, armed=True, long_requested=False).mode, 'lateralOnly')
self.assertFalse(sd.enabled) # Crossing the threshold is not an enable gesture.
for event in (log.OnroadEvent.EventName.canError, log.OnroadEvent.EventName.accFaulted,
log.OnroadEvent.EventName.calibrationInvalid):
with self.subTest(event=event):
blocked = step(1.385523796, armed=True, long_requested=False, event=event)
self.assertFalse(blocked.desired_lateral or blocked.desired_longitudinal)
state.steerFaultTemporary = True
self.assertFalse(step(1.385523796, armed=True, long_requested=False).lateral_active)
state.steerFaultTemporary = False
sm.seen['aolSafetyWire'] = False
self.assertEqual(step(1.385523796, armed=True, long_requested=False).mode, 'off')
def test_seatbelt_blocks_standard_longitudinal_without_disarming_aol(self):
from openpilot.starpilot.nostalgia import aol_no_entry
owner = AolCardIntent(AolSettings(True, 0, 9, 0, (0, 0, 0), (0, 0, 0)), explicit_latch=True)
state = car_state()
state.cruiseState.available = True
state.seatbeltUnlatched = True
events = Events()
events.add(log.OnroadEvent.EventName.seatbeltNotLatched)
self.assertTrue(events.contains(ET.NO_ENTRY))
self.assertTrue(events.contains(ET.SOFT_DISABLE))
owner.update(state, fault_active=disarming_fault(events.to_msg(), state))
self.assertFalse(owner.allowed_latch)
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=True)]
owner.update(state, fault_active=disarming_fault(events.to_msg(), state))
self.assertTrue(owner.allowed_latch)
state.buttonEvents = []
standard = StateMachine()
events.add(log.OnroadEvent.EventName.buttonEnable)
self.assertEqual(standard.update(events), (False, False))
native = SimpleNamespace(requestedLateral=True, requestedLongitudinal=False,
lateralAllowed=True, longitudinalAllowed=True)
for unlatched in (True, True, False):
state.seatbeltUnlatched = unlatched
events.clear()
if unlatched:
events.add(log.OnroadEvent.EventName.seatbeltNotLatched)
owner.update(state, fault_active=disarming_fault(events.to_msg(), state))
enabled, active = standard.update(events)
self.assertFalse(enabled or active) # Buckling alone is not an engagement gesture.
allowed, pause_lat, pause_long = owner.output(state)
decision = decide_axes(standard_lateral=active, standard_longitudinal=enabled,
intent=SimpleNamespace(allowedLatch=allowed, pauseLateral=pause_lat, pauseLongitudinal=pause_long),
native=native, car_state=state, initialized=True, model_ready=True,
no_entry=aol_no_entry(events.names, state, paddle_only_cancel=False),
immediate_disable=False, dm_lockout=False, pause_brake_mps=0.)
self.assertTrue(decision.lateral_active)
self.assertFalse(decision.desired_longitudinal or decision.longitudinal_active)
state.seatbeltUnlatched = True
direct = decide_axes(standard_lateral=True, standard_longitudinal=True,
intent=SimpleNamespace(allowedLatch=True, pauseLateral=False, pauseLongitudinal=False),
native=native, car_state=state, initialized=True, model_ready=True, no_entry=False,
immediate_disable=False, dm_lockout=False, pause_brake_mps=0.)
self.assertTrue(direct.lateral_active)
self.assertFalse(direct.desired_longitudinal)
events.add(log.OnroadEvent.EventName.doorOpen)
self.assertFalse(disarming_fault(events.to_msg(), state))
self.assertTrue(aol_no_entry(events.names, state, paddle_only_cancel=False))
def test_temporary_eps_and_known_gears_preserve_latch_with_zero_steering(self):
owner = AolCardIntent(AolSettings(True, 0, 0, 0, (0, 0, 0), (0, 0, 0)), explicit_latch=True)
state = car_state()
owner.update(state)
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=True)]
owner.update(state)
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=False)]
owner.update(state)
state.buttonEvents = []
for gear, temporary, name in (
(car.CarState.GearShifter.drive, True, log.OnroadEvent.EventName.steerTempUnavailable),
(car.CarState.GearShifter.neutral, False, log.OnroadEvent.EventName.wrongGear),
(car.CarState.GearShifter.reverse, False, log.OnroadEvent.EventName.wrongGear),
(car.CarState.GearShifter.park, False, log.OnroadEvent.EventName.wrongGear)):
with self.subTest(gear=gear, temporary=temporary):
state.gearShifter = gear
state.steerFaultTemporary = temporary
events = Events()
events.add(name)
fault = disarming_fault(events.to_msg(), state)
self.assertFalse(fault)
for _ in range(100):
owner.update(state, fault_active=fault)
self.assertTrue(owner.allowed_latch)
allowed, pause_lat, pause_long = owner.output(state)
decision = decide_axes(standard_lateral=False, standard_longitudinal=False,
intent=SimpleNamespace(allowedLatch=allowed, pauseLateral=pause_lat, pauseLongitudinal=pause_long),
native=SimpleNamespace(requestedLateral=False, requestedLongitudinal=False, lateralAllowed=False, longitudinalAllowed=False),
car_state=state, initialized=True, model_ready=True, no_entry=True, immediate_disable=False,
dm_lockout=False, pause_brake_mps=0)
self.assertFalse(decision.desired_lateral)
self.assertFalse(decision.lateral_active)
state.gearShifter = car.CarState.GearShifter.drive
state.steerFaultTemporary = False
owner.update(state, fault_active=False)
self.assertTrue(owner.output(state)[0])
decision = decide_axes(standard_lateral=False, standard_longitudinal=False,
intent=SimpleNamespace(allowedLatch=owner.output(state)[0], pauseLateral=False, pauseLongitudinal=False),
native=SimpleNamespace(requestedLateral=True, requestedLongitudinal=False, lateralAllowed=True, longitudinalAllowed=False),
car_state=state, initialized=True, model_ready=True, no_entry=False, immediate_disable=False,
dm_lockout=False, pause_brake_mps=0)
self.assertTrue(decision.lateral_active)
def test_calibration_pause_retains_intent_and_mapping_change_cannot_rearm(self):
from dataclasses import replace
from openpilot.starpilot.aol.intent import AOL_TOGGLE
owner = AolCardIntent(AolSettings(True, 0., 0, 0, (0, 0, 0), (0, 0, 0)))
state = car_state()
state.cruiseState.available = True
owner.update(state)
owner.observe_calibration(state, events=[])
self.assertTrue(owner.allowed_latch)
invalid = Events()
invalid.add(log.OnroadEvent.EventName.calibrationInvalid)
owner.observe_calibration(state, events=invalid.to_msg())
self.assertTrue(owner.allowed_latch)
self.assertEqual(owner.output(state)[1:], (True, True))
owner.settings = replace(owner.settings, lkas_action=AOL_TOGGLE)
owner.update(state)
owner.observe_calibration(state, events=[])
self.assertTrue(owner.allowed_latch)
self.assertEqual(owner.output(state)[1:], (False, False))
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=True)]
owner.update(state)
self.assertFalse(owner.allowed_latch)
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=False)]
owner.update(state)
state.buttonEvents = []
owner.settings = replace(owner.settings, main_action=AOL_TOGGLE)
owner.update(state)
owner.settings = replace(owner.settings, main_action=0)
owner.update(state)
self.assertFalse(owner.allowed_latch) # Changing another mapping is not an arming gesture.
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=True)]
owner.update(state)
self.assertTrue(owner.allowed_latch)
def test_low_speed_brake_suppresses_output_without_destroying_explicit_latch(self):
owner = AolCardIntent(AolSettings(True, 5.0, 0, 0, (0, 0, 0), (0, 0, 0)), explicit_latch=True)
state = car_state()
owner.update(state)
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=True)]
owner.update(state)
state.buttonEvents = []
events = Events()
events.add(log.OnroadEvent.EventName.pedalPressed)
for speed, brake, expected in ((2., True, False), (2., False, True), (6., True, True)):
state.vEgo, state.brakePressed = speed, brake
owner.update(state, fault_active=disarming_fault(events.to_msg(), state))
self.assertTrue(owner.allowed_latch)
allowed, pause_lat, pause_long = owner.output(state)
axes = decide_axes(standard_lateral=False, standard_longitudinal=False,
intent=SimpleNamespace(allowedLatch=allowed, pauseLateral=pause_lat, pauseLongitudinal=pause_long),
native=SimpleNamespace(requestedLateral=expected, requestedLongitudinal=False,
lateralAllowed=expected, longitudinalAllowed=False),
car_state=state, initialized=True, model_ready=True, no_entry=False, immediate_disable=False,
dm_lockout=False, pause_brake_mps=5.0)
self.assertEqual(axes.lateral_active, expected)
self.assertFalse(axes.longitudinal_active)
def test_permanent_and_unrelated_faults_still_disarm(self):
state = car_state()
for name in (log.OnroadEvent.EventName.steerUnavailable, log.OnroadEvent.EventName.controlsMismatch,
log.OnroadEvent.EventName.overheat):
events = Events()
events.add(name)
self.assertEqual(disarming_fault(events.to_msg(), state),
any(event.immediateDisable for event in events.to_msg()))
events = Events()
events.add(log.OnroadEvent.EventName.wrongGear)
self.assertFalse(disarming_fault(events.to_msg(), state)) # Events may lag the return to Drive.
state.gearShifter = car.CarState.GearShifter.unknown
self.assertTrue(disarming_fault(events.to_msg(), state))
state.steerFaultPermanent = True
self.assertTrue(disarming_fault([], state))
owner = AolCardIntent(AolSettings(True, 0, 0, 0, (0, 0, 0), (0, 0, 0)), explicit_latch=True)
owner.allowed_latch = True
owner.update(state)
self.assertFalse(owner.allowed_latch)
def test_native_rejection_rearms_with_one_new_press_without_replaying_old_receipt(self):
owner = AolCardIntent(AolSettings(True, 0, 0, 0, (0, 0, 0), (0, 0, 0)), explicit_latch=True)
state = car_state()
lkas = car.CarState.ButtonEvent.Type.lkas
owner.update(state, now_ns=100)
state.buttonEvents = [car.CarState.ButtonEvent(type=lkas, pressed=True)]
owner.update(state, now_ns=200)
self.assertTrue(owner.allowed_latch)
state.buttonEvents = [car.CarState.ButtonEvent(type=lkas, pressed=False)]
owner.update(state, now_ns=300)
state.buttonEvents = []
owner.update(state, now_ns=400, native_rejection_ns=350)
self.assertFalse(owner.allowed_latch)
state.buttonEvents = [car.CarState.ButtonEvent(type=lkas, pressed=True)]
owner.update(state, now_ns=500, native_rejection_ns=350)
self.assertTrue(owner.allowed_latch)
state.buttonEvents = []
owner.update(state, now_ns=600, native_rejection_ns=450)
self.assertTrue(owner.allowed_latch) # Delayed denial predates the new press.
owner.update(state, now_ns=700, native_rejection_ns=650)
self.assertFalse(owner.allowed_latch)
owner.update(state, now_ns=800)
self.assertFalse(owner.allowed_latch) # The held physical button cannot rearm.
state.buttonEvents = [car.CarState.ButtonEvent(type=lkas, pressed=False)]
owner.update(state, now_ns=900)
state.buttonEvents = [car.CarState.ButtonEvent(type=lkas, pressed=True)]
owner.update(state, now_ns=1000)
self.assertTrue(owner.allowed_latch)
def test_native_rejection_and_fresh_press_same_frame_preserve_fault_inhibit(self):
for fault in (False, True):
owner = AolCardIntent(AolSettings(True, 0, 0, 0, (0, 0, 0), (0, 0, 0)), explicit_latch=True)
state = car_state()
owner.update(state, now_ns=100)
owner.allowed_latch = True
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=True)]
owner.update(state, now_ns=200, native_rejection_ns=150, fault_active=fault)
self.assertEqual(owner.allowed_latch, not fault)
def test_explicit_latch_requires_new_gesture_after_fault(self):
owner = AolCardIntent(AolSettings(True, 0, 0, 0, (0, 0, 0), (0, 0, 0)), explicit_latch=True)
state = car_state()
lkas = car.CarState.ButtonEvent.Type.lkas
owner.update(state, fault_active=False)
state.buttonEvents = [car.CarState.ButtonEvent(type=lkas, pressed=True)]
owner.update(state)
self.assertTrue(owner.allowed_latch)
state.buttonEvents = []
owner.update(state, fault_active=True)
self.assertFalse(owner.allowed_latch)
state.buttonEvents = [car.CarState.ButtonEvent(type=lkas, pressed=False)]
owner.update(state, fault_active=True)
state.buttonEvents = [car.CarState.ButtonEvent(type=lkas, pressed=True)]
owner.update(state, fault_active=True)
self.assertFalse(owner.allowed_latch)
state.buttonEvents = [car.CarState.ButtonEvent(type=lkas, pressed=False)]
owner.update(state)
state.buttonEvents = [car.CarState.ButtonEvent(type=lkas, pressed=True)]
owner.update(state) # No fresh clear event yet.
self.assertFalse(owner.allowed_latch)
# A held button at recovery is not a new request. The driver must release it first.
owner.update(state, fault_active=False)
self.assertFalse(owner.allowed_latch)
state.buttonEvents = [car.CarState.ButtonEvent(type=lkas, pressed=False)]
owner.update(state)
state.buttonEvents = [car.CarState.ButtonEvent(type=lkas, pressed=True)]
owner.update(state)
self.assertTrue(owner.allowed_latch)
def test_persisted_preferences_and_fresh_defaults(self):
with OpenpilotPrefix():
params = Params()
fresh = read_settings(params)
self.assertFalse(fresh.enabled)
self.assertEqual(fresh.pause_brake_mps, 0.0)
self.assertEqual(fresh.distance_actions, (0, 0, 0))
params.put_bool('AlwaysOnLateral', True, block=True)
params.put('PauseAOLOnBrake', 10.0, block=True)
params.put('DistanceButtonControl', 3, block=True)
saved = read_settings(params)
self.assertTrue(saved.enabled)
self.assertEqual(saved.pause_brake_mps, 10.0)
self.assertEqual(saved.distance_actions, (3, 0, 0))
def test_brake_pause_units_preserve_legacy_until_explicit_si_override(self):
with OpenpilotPrefix():
params = Params()
params.put_bool('AlwaysOnLateral', True, block=True)
params.put('PauseAOLOnBrake', 10.0, block=True)
for metric in (False, True):
params.put_bool('IsMetric', metric, block=True)
self.assertEqual(read_settings(params).pause_brake_mps, 10.0)
params.put('AolBrakePauseSpeedMps', 4.4704, block=True)
self.assertAlmostEqual(read_settings(params).pause_brake_mps, 4.4704)
self.assertTrue(read_settings(params).enabled)
params.put('AolBrakePauseSpeedMps', 0.0, block=True)
self.assertEqual(read_settings(params).pause_brake_mps, 0.0)
params.put('AolBrakePauseSpeedMps', -1.0, block=True)
self.assertFalse(read_settings(params).enabled)
self.assertTrue(params.get_bool('AlwaysOnLateral'))
params.remove('AolBrakePauseSpeedMps')
self.assertEqual(read_settings(params).pause_brake_mps, 10.0)
def test_invalid_threshold_never_falls_back_to_permissive_value(self):
with OpenpilotPrefix():
params = Params()
params.put_bool('AlwaysOnLateral', True, block=True)
params.put('PauseAOLOnBrake', 10.0, block=True)
for key in ('AolBrakePauseSpeedMps', 'PauseAOLOnBrake'):
path = Path(params.get_param_path(key))
for value in (b'', b'bad', b'\xff', b'nan', b'inf', b'-1', b'100.1'):
with self.subTest(key=key, value=value):
path.write_bytes(value)
self.assertFalse(read_settings(params).enabled)
self.assertTrue(params.get_bool('AlwaysOnLateral'))
path.unlink()
self.assertTrue(read_settings(params).enabled)
self.assertEqual(read_settings(params).pause_brake_mps, 0.0)
def test_saved_threshold_controls_actual_brake_boundary(self):
with OpenpilotPrefix():
params = Params()
params.put_bool('AlwaysOnLateral', True, block=True)
params.put('PauseAOLOnBrake', 10.0, block=True)
state = car_state(brake=True)
native = SimpleNamespace(requestedLateral=True, requestedLongitudinal=False,
lateralAllowed=True, longitudinalAllowed=False)
intent = SimpleNamespace(allowedLatch=True, lateralArmed=True, pauseLateral=False, pauseLongitudinal=False)
for metric in (False, True):
params.put_bool('IsMetric', metric, block=True)
for speed, mode in ((9.9, 'off'), (10.0, 'lateralOnly')):
state.vEgo = speed
result = decide_axes(standard_lateral=False, standard_longitudinal=False, intent=intent,
native=native, car_state=state, initialized=True, model_ready=True,
no_entry=False, immediate_disable=False, dm_lockout=False,
pause_brake_mps=read_settings(params).pause_brake_mps)
self.assertEqual(result.mode, mode)
def test_honda_main_lkas_pause_and_cancel_ownership(self):
settings = AolSettings(True, 5.0, 9, 0, (3, 4, 0), (3, 0, 0))
owner = AolCardIntent(settings)
state = car_state()
state.cruiseState.available = True
owner.update(state)
self.assertFalse(owner.output(state)[0]) # LKAS button owns the latch
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=True)]
owner.update(state)
self.assertTrue(owner.output(state)[0])
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.cancel, pressed=True)]
owner.update(state)
self.assertFalse(owner.pause_lateral) # Honda cancel never doubles as pause
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.gapAdjustCruise, pressed=True)]
owner.update(state)
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.gapAdjustCruise, pressed=False)]
owner.update(state)
self.assertTrue(owner.pause_lateral)
self.assertEqual(owner.output(state), (True, True, False))
def test_invalid_can_and_settings_disable_intent(self):
owner = AolCardIntent(AolSettings(True, 0, 0, 0, (0, 0, 0), (0, 0, 0)))
state = car_state()
state.cruiseState.available = True
owner.update(state)
self.assertTrue(owner.output(state)[0])
state.canValid = False
owner.update(state)
self.assertFalse(owner.output(state)[0])
def test_interrupted_distance_hold_does_not_replay_and_keeps_deliberate_pause(self):
settings = AolSettings(True, 0, 0, 0, (3, 4, 0), (0, 0, 0))
owner = AolCardIntent(settings)
state = car_state()
state.cruiseState.available = True
button = car.CarState.ButtonEvent.Type.gapAdjustCruise
owner.update(state)
state.buttonEvents = [car.CarState.ButtonEvent(type=button, pressed=True)]
owner.update(state)
state.buttonEvents = []
for _ in range(5):
owner.update(state)
state.canValid = False
owner.update(state)
self.assertFalse(owner._held[int(button)])
self.assertEqual(owner._timers[int(button)], 0)
state.canValid = True
state.buttonEvents = [car.CarState.ButtonEvent(type=button, pressed=False)]
owner.update(state)
self.assertFalse(owner.pause_lateral or owner.pause_longitudinal)
# A complete user pause remains latched through CAN loss.
state.buttonEvents = [car.CarState.ButtonEvent(type=button, pressed=True)]
owner.update(state)
state.buttonEvents = [car.CarState.ButtonEvent(type=button, pressed=False)]
owner.update(state)
self.assertTrue(owner.pause_lateral)
state.canValid = False
owner.update(state)
self.assertTrue(owner.pause_lateral)
# If another owner consumed a release, it cannot complete the held gesture.
state.canValid = True
state.buttonEvents = [car.CarState.ButtonEvent(type=button, pressed=True)]
owner.update(state)
state.buttonEvents = [car.CarState.ButtonEvent(type=button, pressed=False)]
owner.update(state, consumed_buttons=frozenset((int(button),)))
self.assertFalse(owner._held[int(button)])
self.assertTrue(owner.pause_lateral)
# CAN loss just before the long-press boundary cannot finish that gesture.
state.buttonEvents = [car.CarState.ButtonEvent(type=button, pressed=True)]
owner.update(state)
state.buttonEvents = []
for _ in range(CRUISE_LONG_PRESS - 2):
owner.update(state)
self.assertFalse(owner.pause_longitudinal)
state.canValid = False
owner.update(state)
state.canValid = True
for _ in range(3):
owner.update(state)
self.assertFalse(owner.pause_longitudinal)
self.assertTrue(owner.pause_lateral)
# An owner can reserve the button on a tick with no new button event.
state.buttonEvents = [car.CarState.ButtonEvent(type=button, pressed=True)]
owner.update(state)
state.buttonEvents = []
for _ in range(CRUISE_LONG_PRESS - 2):
owner.update(state)
owner.update(state, consumed_buttons=frozenset((int(button),)))
state.buttonEvents = [car.CarState.ButtonEvent(type=button, pressed=False)]
owner.update(state)
self.assertFalse(owner.pause_longitudinal)
self.assertTrue(owner.pause_lateral)
def test_held_at_start_distance_waits_for_neutral_before_all_press_lengths(self):
button = car.CarState.ButtonEvent.Type.gapAdjustCruise
for held_ticks, actions in ((1, (3, 0, 0)), (CRUISE_LONG_PRESS, (0, 4, 0)),
(CRUISE_LONG_PRESS * 5, (0, 0, 3))):
with self.subTest(held_ticks=held_ticks):
settings = AolSettings(True, 0, 0, 0, actions, (0, 0, 0))
owner = AolCardIntent(settings, explicit_latch=True)
state = car_state()
state.buttonEvents = [car.CarState.ButtonEvent(type=button, pressed=True)]
owner.update(state)
state.buttonEvents = []
for _ in range(held_ticks):
owner.update(state)
state.buttonEvents = [car.CarState.ButtonEvent(type=button, pressed=False)]
owner.update(state)
self.assertFalse(owner.pause_lateral or owner.pause_longitudinal)
state.buttonEvents = [car.CarState.ButtonEvent(type=button, pressed=True)]
owner.update(state)
state.buttonEvents = []
for _ in range(held_ticks - 1):
owner.update(state)
state.buttonEvents = [car.CarState.ButtonEvent(type=button, pressed=False)]
owner.update(state)
self.assertEqual((owner.pause_lateral, owner.pause_longitudinal),
(held_ticks != CRUISE_LONG_PRESS, held_ticks == CRUISE_LONG_PRESS))
class IpcAxisContractTests(unittest.TestCase):
_authority_callers = availability_fixture.IoniqAvailabilityTests._authority_callers
_ioniq_availability_callers = availability_fixture.IoniqAvailabilityTests._ioniq_availability_callers
def test_fresh_ioniq_input_gets_full_publication_lease(self):
from openpilot.starpilot.aol.runtime import current_axis
with OpenpilotPrefix():
sd, controls, step, _, _ = self._ioniq_availability_callers()
step(2)
source = sd.aol_car_state_log_ns
published = source + 29_960_000
with mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=published):
sd.publish_axis_proof(sd.CS_prev)
proof = sd.pm.messages['aolAxisState']
self.assertEqual(proof.aolAxisState.sourceCarStateMonoTime, source)
self.assertEqual(proof.aolAxisState.observedMonoTime, published)
self.assertEqual(proof.aolAxisState.validUntilMonoTime, published + 30_000_000)
self.assertTrue(proof.aolAxisState.lateralActive and proof.aolAxisState.longitudinalActive)
controls.sm.update_msgs(published / 1e9, [proof])
# Source age has crossed 30 ms; this already-admitted output is still fresh.
for now in (source + 33_507_000, published + 30_000_000):
self.assertIsNotNone(current_axis(controls.sm, now_ns=now))
with mock.patch('openpilot.selfdrive.controls.controlsd.time.monotonic_ns', return_value=now):
command, _ = controls.state_control()
self.assertTrue(command.latActive and command.longActive)
now = published + 30_000_001
self.assertIsNone(current_axis(controls.sm, now_ns=now))
with mock.patch('openpilot.selfdrive.controls.controlsd.time.monotonic_ns', return_value=now):
command, _ = controls.state_control()
self.assertFalse(command.latActive or command.longActive)
def test_expired_ioniq_input_has_inactive_publication_lease(self):
from openpilot.starpilot.aol.runtime import current_axis
for age in (30_008_000, 30_055_000):
with self.subTest(source_age_ns=age), OpenpilotPrefix():
sd, controls, step, _, _ = self._ioniq_availability_callers()
step(2)
published = sd.aol_car_state_log_ns + age
with mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=published):
sd.publish_axis_proof(sd.CS_prev)
proof = sd.pm.messages['aolAxisState']
axis = proof.aolAxisState
self.assertEqual(axis.validUntilMonoTime, published + 30_000_000)
self.assertEqual(str(axis.faultReason), 'availabilityPause')
self.assertEqual(axis.sourceCarStateMonoTime, 0)
self.assertFalse(axis.lateralActive or axis.longitudinalActive or axis.desiredLongitudinal)
availability_fixture.assert_ordinary_source_pause(sd, publication_only=True)
controls.sm.update_msgs(published / 1e9, [proof])
# A fresh output lease does not make stale input eligible for actuation.
self.assertIsNotNone(current_axis(controls.sm, now_ns=published))
with mock.patch('openpilot.selfdrive.controls.controlsd.time.monotonic_ns', return_value=published):
command, _ = controls.state_control()
self.assertFalse(command.latActive or command.longActive)
self.assertIsNone(current_axis(controls.sm, now_ns=published + 30_000_001))
recovered, _, recovered_command = step(6)
availability_fixture.assert_fresh_ordinary_recovery(sd, recovered, recovered_command, 10_060_000_000)
self.assertEqual(axis.validUntilMonoTime, published + 30_000_000)
def test_publication_lease_preserves_ioniq_fault_denial(self):
for fault in ('native_missing', 'native_denied', 'can_invalid', 'permanent_eps'):
with self.subTest(fault=fault), OpenpilotPrefix():
sd, controls, step, _, _ = self._ioniq_availability_callers()
step(2)
observed = car_state()
kwargs = {}
if fault == 'native_missing':
kwargs['native_valid'] = False
elif fault == 'native_denied':
kwargs['allowed'] = False
elif fault == 'can_invalid':
observed.canValid = False
else:
observed.steerFaultPermanent = True
step(3, observed_state=observed, **kwargs)
published = 10_040_000_000
with mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=published):
sd.publish_axis_proof(sd.CS_prev)
proof = sd.pm.messages['aolAxisState']
self.assertEqual(proof.aolAxisState.validUntilMonoTime, published + 30_000_000)
self.assertFalse(proof.aolAxisState.lateralActive or proof.aolAxisState.longitudinalActive)
controls.sm.update_msgs(published / 1e9, [proof])
with mock.patch('openpilot.selfdrive.controls.controlsd.time.monotonic_ns', return_value=published):
command, _ = controls.state_control()
self.assertFalse(command.latActive or command.longActive)
def test_ioniq6_fresh_backlog_companion_keeps_axes_and_stale_loss_warns(self):
from opendbc.car import gen_empty_fingerprint
from opendbc.car.hyundai.interface import CarInterface
from opendbc.car.hyundai.ioniq6_handoff import build_ioniq6_hda2_long_candidate
from opendbc.car.hyundai.values import CAR
from openpilot.starpilot.aol.intent_companion import IntentCompanion
for full_op in (False, True):
with self.subTest(full_op=full_op), OpenpilotPrefix():
fp = gen_empty_fingerprint()
fp[2].update({0x110: 32, 0x362: 32})
fp[0][0x3A5] = 24
fp[1].update({0x1CF: 8, 0x1AA: 16, 0x35: 32, 0x175: 24, 0xA0: 24, 0xEA: 24,
0x1BA: 24, 0x1E5: 16, 0x36A: 16})
stock = CarInterface.get_params(CAR.HYUNDAI_IONIQ_6, fp, [], False, False, False)
cp = build_ioniq6_hda2_long_candidate(stock, fp)
self.assertIsNotNone(cp)
# Mirror Card's saved-AOL marker before publishing the LONG candidate.
cp.safetyConfigs[-1].safetyParam |= 0x0800
pm = messaging.PubMaster(['aolIntentWire'])
sd, controls, step = self._authority_callers(full_op, cp=cp)
self.assertIsInstance(sd.aol_intent_companion, IntentCompanion)
self.assertEqual(sd.CP.safetyConfigs[-1].safetyParam, 0x8895)
self.assertEqual(sd.CP.alternativeExperience, 0)
axis, _, command = step(0, offset_ns=50_000_000)
self.assertTrue(axis.lateralActive and command.latActive)
self.assertEqual(command.longActive, full_op)
base = 10_000_000_000
def intent(stamp, sequence):
message = messaging.new_message('aolIntentWire', 0, valid=True, logMonoTime=stamp)
message.aolIntentWire = encode_intent(IntentState('authority-card', sequence, stamp, stamp,
stamp + 200_000_000, True, False, False, True, True))
return message
source = base + 66_000_000
first = intent(source, 2)
newer = intent(base + 77_000_000, 3)
pm.send('aolIntentWire', first)
pm.send('aolIntentWire', newer)
axis, state, command = step(8, state_stamp=source, intent_message=newer)
self.assertEqual(axis.sourceCarStateMonoTime, source)
self.assertTrue(axis.nativeAcknowledged and axis.lateralActive and command.latActive)
self.assertEqual(axis.longitudinalActive, full_op)
self.assertEqual(command.longActive, full_op)
self.assertNotIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
self.assertNotEqual(state.alertText1, 'TAKE CONTROL IMMEDIATELY')
self.assertTrue(step(9)[0].lateralActive)
axis, state, command = step(17, timeout=True, offset_ns=877_165)
self.assertEqual(axis.sourceCarStateMonoTime, 0)
self.assertFalse(sd.enabled or sd.active or axis.lateralActive or axis.longitudinalActive)
self.assertFalse(command.latActive or command.longActive)
self.assertIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
self.assertEqual(state.alertText1, 'TAKE CONTROL IMMEDIATELY')
self._assert_withdrawal_alert(controls, sd.pm.messages['selfdriveState'])
self.assertEqual(str(axis.faultReason), 'transportPause')
self.assertFalse(sd.aol_authority_lost)
recovered, _, command = step(18, native_request_long=False)
self.assertTrue(recovered.lateralActive and command.latActive)
self.assertFalse(sd.enabled or sd.active or recovered.longitudinalActive or command.longActive)
def _transport_callers(self):
from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.car.tests.test_hyundai_aol import candidate
from openpilot.starpilot.aol.intent import AolProcessFaultContext
from openpilot.starpilot.aol.transport_pause import TransportPauseFeedback
from openpilot.starpilot.car.hyundai.aol import create_intent
_, cp = candidate(True)
cp.safetyConfigs[-1].safetyParam |= 0x0800
sd, controls, step = self._authority_callers(True, cp=cp)
card = Car.__new__(Car)
card.CP = cp.as_reader()
# Match the actual Card producer behind this fixture's raw intent wire.
card.slc_producer_session = 'authority-card'
services = ['aolAxisState', 'aolSafetyWire', 'pandaStates', 'onroadEvents']
self.enterContext(mock.patch.object(card, 'sm', messaging.SubMaster(services, ignore_avg_freq=services), create=True))
card.aol_process_fault_context = AolProcessFaultContext()
card.aol_transport_feedback = TransportPauseFeedback(card.CP)
card.aol_card_intent = create_intent(cp, AolSettings(True, 0., 9, 0, (0, 0, 0), (0, 0, 0)))
cs = car_state()
cs.cruiseState.available = True
card.aol_card_intent.update(cs, now_ns=100)
cs.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=True)]
card.aol_card_intent.update(cs, now_ns=200)
cs.buttonEvents = []
self.assertTrue(card.aol_card_intent.allowed_latch)
def feedback(now, *, axis=None, event=None, state=None):
frames = []
for service in ('pandaStates', 'aolSafetyWire'):
message = messaging.new_message(service, len(sd.sm[service]), valid=sd.sm.valid[service],
logMonoTime=sd.sm.logMonoTime[service])
setattr(message, service, sd.sm[service])
frames.append(message.as_reader())
frames += [m for m in (axis, event) if m is not None]
card.sm.update_msgs(now / 1e9, frames)
fault = card.aol_disarming_fault(cs if state is None else state,
int(card.sm.logMonoTime['onroadEvents']), now)
card.aol_card_intent.update(cs if state is None else state, fault_active=fault, now_ns=now,
standard_enabled=sd.enabled)
return fault
return sd, controls, step, card, feedback
def test_actual_hkg_transport_pause_preserves_intent_and_recovers_only_lateral(self):
from openpilot.starpilot.aol.intent_companion import IntentCompanion
with OpenpilotPrefix():
sd, controls, step, card, feedback = self._transport_callers()
self.assertIsInstance(sd.aol_intent_companion, IntentCompanion)
axis, _, command = step(0)
self.assertTrue(axis.lateralActive and axis.longitudinalActive and command.longActive)
feedback(10_000_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
axis, _, command = step(8, timeout=True, offset_ns=877_165)
now = int(sd.pm.messages['aolAxisState'].logMonoTime)
self.assertEqual(str(axis.faultReason), 'transportPause')
self.assertTrue(sd.pm.messages['aolAxisState'].valid)
self.assertFalse(sd.enabled or sd.active or command.latActive or command.longActive)
self._assert_withdrawal_alert(controls, sd.pm.messages['selfdriveState'])
self.assertFalse(feedback(now, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents']))
self.assertTrue(card.aol_card_intent.allowed_latch)
axis, _, command = step(9, native_request_lat=False, native_request_long=False)
self.assertTrue(axis.desiredLateral)
self.assertFalse(axis.lateralActive or axis.longitudinalActive or command.latActive or command.longActive)
self.assertEqual(str(axis.faultReason), 'transportPause')
now = int(sd.pm.messages['aolAxisState'].logMonoTime)
self.assertFalse(feedback(now, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents']))
self.assertTrue(card.aol_card_intent.allowed_latch)
axis, _, command = step(10, native_request_long=False)
self.assertEqual(str(axis.faultReason), 'none')
self.assertTrue(axis.lateralActive and command.latActive)
self.assertFalse(sd.enabled or sd.active or axis.longitudinalActive or command.longActive)
now = int(sd.pm.messages['aolAxisState'].logMonoTime)
feedback(now, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
self.assertTrue(card.aol_card_intent.allowed_latch)
self.assertFalse(step(11, latched=False, native_request_long=False)[0].lateralActive)
cs = car_state()
from dataclasses import replace
card.aol_card_intent.settings = replace(card.aol_card_intent.settings, cancel_actions=(3, 0, 0))
cs.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.cancel, pressed=True)]
card.aol_card_intent.update(cs, now_ns=now + 10_000_000)
self.assertFalse(card.aol_card_intent.allowed_latch)
def test_transport_pause_hard_upgrades_never_recover_without_withdrawal(self):
cases = ('native_missing', 'native_deny', 'panda_rx', 'calibration', 'permanent_eps',
'can_timeout', 'critical', 'producer_restart', 'invalid_then_fresh', 'cp_change', 'session_change')
for case in cases:
with self.subTest(case=case), OpenpilotPrefix():
sd, _, step, _, _ = self._transport_callers()
self.assertTrue(step(0)[0].longitudinalActive)
self.assertEqual(str(step(8, timeout=True)[0].faultReason), 'transportPause')
kwargs = {'native_request_long': False}
if case == 'native_missing':
kwargs['native_valid'] = False
elif case == 'native_deny':
kwargs['allowed'] = False
elif case == 'panda_rx':
kwargs['panda_fault'] = True
elif case == 'calibration':
kwargs['calibrated'] = False
elif case in ('permanent_eps', 'can_timeout'):
state = car_state()
if case == 'permanent_eps':
state.steerFaultPermanent = True
else:
state.canTimeout = True
kwargs['observed_state'] = state
elif case == 'critical':
kwargs['critical_event'] = log.OnroadEvent.EventName.controlsMismatch
elif case == 'producer_restart':
now = 10_090_000_000
message = messaging.new_message('aolIntentWire', 0, valid=True, logMonoTime=now)
message.aolIntentWire = encode_intent(IntentState('restarted-card', 1, now, now,
now + 200_000_000, True, False, False, True, True))
kwargs['intent_message'] = message
elif case == 'invalid_then_fresh':
raw = sd.sm['aolIntentWire']
sd.aol_intent_companion._observe(sd.sm.logMonoTime['aolIntentWire'], False, raw)
elif case == 'cp_change':
cp = sd.CP.as_builder()
cp.safetyConfigs[-1].safetyParam ^= 1
sd.CP = cp.as_reader()
else:
sd.aol_session_id = 'restarted-selfdrived'
axis, _, command = step(9, **kwargs)
self.assertEqual(str(axis.faultReason), 'critical')
self.assertTrue(sd.aol_authority_lost)
self.assertFalse(axis.lateralActive or axis.longitudinalActive or command.latActive or command.longActive)
self.assertFalse(step(10, native_request_long=False)[0].lateralActive)
def test_actual_card_feedback_pairing_both_orders_and_fixed_deadline(self):
for order in ('axis_first', 'event_first', 'together', 'missing_event', 'missing_axis',
'renew_event', 'unknown', 'critical', 'wrong_session', 'restart', 'card_restart',
'mixed_fault', 'disabled', 'can_timeout'):
with self.subTest(order=order), OpenpilotPrefix():
sd, _, step, card, feedback = self._transport_callers()
step(0)
feedback(10_000_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
step(2)
feedback(10_020_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
step(4, timeout=True)
feedback(10_040_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
step(6, timeout=True)
now = 10_060_000_000
proof, event = sd.pm.messages['aolAxisState'], sd.pm.messages['onroadEvents']
# The actual last retained-source publication is 20 ms old. Deliver
# these real publisher envelopes separately in either socket order.
if order == 'axis_first':
self.assertFalse(feedback(now, axis=proof))
self.assertFalse(card.aol_transport_feedback.qualified)
self.assertFalse(feedback(now, event=event))
elif order == 'event_first':
self.assertFalse(feedback(now, event=event))
self.assertTrue(card.aol_transport_feedback.awaiting)
self.assertFalse(feedback(now, axis=proof))
elif order == 'together':
self.assertFalse(feedback(now, axis=proof, event=event))
elif order in ('missing_event', 'missing_axis', 'renew_event'):
self.assertFalse(feedback(now, axis=proof if order == 'missing_event' else None,
event=event if order != 'missing_event' else None))
step(9, native_request_lat=False, native_request_long=False)
renewed = sd.pm.messages['aolAxisState']
next_event = event
if order == 'renew_event':
next_event = event.as_builder()
next_event.logMonoTime = now + 10_000_000
next_event = next_event.as_reader()
# Event occurrences and fresh axis streams cannot renew a pending
# exact pair beyond the first occurrence's original 30 ms deadline.
self.assertTrue(feedback(now + 31_000_000, axis=renewed if order == 'missing_event' else None,
event=next_event if order == 'renew_event' else None))
self.assertFalse(card.aol_card_intent.allowed_latch)
self.assertTrue(feedback(now + 32_000_000, axis=proof, event=event))
continue
else:
if order == 'can_timeout':
cs = car_state()
cs.canTimeout = True
self.assertTrue(feedback(now, axis=proof, event=event, state=cs))
elif order == 'card_restart':
from openpilot.starpilot.aol.transport_pause import TransportPauseFeedback
card.aol_transport_feedback = TransportPauseFeedback(card.CP)
self.assertTrue(feedback(now, axis=proof, event=event))
elif order == 'mixed_fault':
faults = Events()
faults.add(log.OnroadEvent.EventName.controlsMismatch)
faults.add(log.OnroadEvent.EventName.relayMalfunction)
mixed = messaging.new_message('onroadEvents', len(faults), valid=True, logMonoTime=now)
mixed.onroadEvents = faults.to_msg()
self.assertTrue(feedback(now, axis=proof, event=mixed.as_reader()))
else:
altered = proof.as_builder()
if order in ('unknown', 'critical'):
altered.aolAxisState.faultReason = order
elif order in ('wrong_session', 'restart'):
altered.aolAxisState.faultSessionId = 'wrong'
if order == 'restart':
altered.aolAxisState.sessionId = 'wrong'
else:
card.aol_card_intent.settings = AolSettings(False, 0., 9, 0, (0, 0, 0), (0, 0, 0))
fault = feedback(now, axis=altered.as_reader(), event=event)
if order != 'disabled':
self.assertTrue(fault)
self.assertFalse(card.aol_card_intent.allowed_latch)
continue
self.assertTrue(card.aol_card_intent.allowed_latch)
self.assertTrue(card.aol_transport_feedback.qualified)
def test_actual_card_unknown_and_missing_native_after_healthy_baseline_disarm(self):
for failure in ('unknown', 'source_zero', 'ack_false', 'native_missing', 'native_deny', 'panda_rx'):
with self.subTest(failure=failure), OpenpilotPrefix():
sd, _, step, card, feedback = self._transport_callers()
step(0)
feedback(10_000_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
kwargs = {'native_valid': False} if failure == 'native_missing' else {'allowed': False} if failure == 'native_deny' else {
'panda_fault': True} if failure == 'panda_rx' else {}
step(1, **kwargs)
proof = sd.pm.messages['aolAxisState']
if failure in ('unknown', 'source_zero', 'ack_false'):
proof = proof.as_builder()
if failure == 'unknown':
proof.aolAxisState.faultReason = 'unknown'
elif failure == 'source_zero':
proof.aolAxisState.sourceCarStateMonoTime = 0
else:
proof.aolAxisState.nativeAcknowledged = False
proof = proof.as_reader()
self.assertTrue(feedback(10_010_000_000, axis=proof, event=sd.pm.messages['onroadEvents']))
self.assertFalse(card.aol_card_intent.allowed_latch)
def test_transport_pause_user_intent_off_is_honored_without_resuming_long(self):
with OpenpilotPrefix():
sd, _, step, card, feedback = self._transport_callers()
step(0)
feedback(10_000_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
step(8, timeout=True)
feedback(10_080_000_000, axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
axis, _, command = step(9, latched=False, native_request_long=False)
self.assertFalse(sd.aol_transport_pause.active or sd.enabled or sd.active)
self.assertFalse(axis.lateralActive or axis.longitudinalActive or command.latActive or command.longActive)
self.assertFalse(sd.aol_authority_lost)
cs = car_state()
from dataclasses import replace
card.aol_card_intent.settings = replace(card.aol_card_intent.settings, cancel_actions=(3, 0, 0))
cs.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.cancel, pressed=True)]
card.aol_card_intent.update(cs, now_ns=10_090_000_000)
self.assertFalse(card.aol_card_intent.allowed_latch)
def _assert_withdrawal_alert(self, controls, message):
from openpilot.starpilot.audio.axis_alerts import AxisAlerts
from openpilot.starpilot.ui.runtime_snapshot import current_alert
now = int(message.logMonoTime)
alert = current_alert(controls.sm, now, after_frame=0)
self.assertTrue(alert.critical)
self.assertEqual(alert.size, 'full')
self.assertEqual(alert.text1, 'TAKE CONTROL IMMEDIATELY')
self.assertEqual(AxisAlerts().update(controls.sm, int(message.selfdriveState.alertSound.raw), now),
log.SelfdriveState.AudibleAlert.warningImmediate)
def test_actual_callers_companion_loss_disables_and_requires_intent_withdrawal(self):
for full_op in (False, True):
for loss in ('car_state', 'intent', 'authority_companion', 'wrong_stamp', 'wrong_source', 'invalid'):
with self.subTest(full_op=full_op, loss=loss), OpenpilotPrefix():
sd, controls, step = self._authority_callers(full_op)
axis, _, command = step(0)
self.assertTrue(axis.lateralActive and axis.nativeAcknowledged and command.latActive)
self.assertAlmostEqual(command.actuators.torque, 0.)
retained, _, command = step(2, timeout=True)
self.assertTrue(retained.lateralActive and command.latActive)
self.assertNotIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
self.assertEqual(retained.sourceCarStateMonoTime, 10_000_000_000)
axis, state, command = step(7, timeout=loss == 'car_state', old_intent=loss == 'intent', companion=False if loss == 'authority_companion' else loss)
self.assertFalse(sd.enabled or sd.active or axis.lateralActive or axis.longitudinalActive or command.latActive or command.longActive)
self.assertIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
self.assertEqual(state.alertText1, 'TAKE CONTROL IMMEDIATELY')
self.assertEqual(state.alertSound, log.SelfdriveState.AudibleAlert.warningImmediate)
self.assertFalse(controls.pm.messages['carControl'].carControl.latActive)
self._assert_withdrawal_alert(controls, sd.pm.messages['selfdriveState'])
self.assertEqual(axis.sourceCarStateMonoTime, 0 if loss == 'car_state' else 10_070_000_000)
self.assertFalse(sd.pm.messages['aolAxisState'].valid if loss == 'car_state' else axis.lateralActive)
self.assertFalse(step(8)[0].lateralActive) # Fresh input alone cannot restart the latched axis.
self.assertFalse(step(9, latched=False)[0].lateralActive)
self.assertTrue(step(10)[0].lateralActive) # Intent owner supplies its fresh rearm after withdrawal.
def test_actual_callers_fresh_native_denial_preserves_deliberate_pauses(self):
for full_op in (False, True):
for reason in ('denial', 'pause', 'brake', 'steering', 'gas', 'user_disable', 'startup',
*(('long_denial', 'long_pause') if full_op else ())):
with self.subTest(full_op=full_op, reason=reason), OpenpilotPrefix():
sd, controls, step = self._authority_callers(full_op)
if reason != 'startup':
self.assertTrue(step(0)[0].lateralActive)
axis, state, command = step(1, allowed=reason in ('long_denial', 'long_pause'), pause=reason == 'pause',
long_allowed=reason not in ('long_denial', 'long_pause'), pause_long=reason == 'long_pause',
brake=reason == 'brake', steering=reason == 'steering',
gas=reason == 'gas', user_disable=reason == 'user_disable')
self.assertEqual(bool(axis.lateralActive), reason == 'long_pause')
self.assertEqual(bool(command.latActive), reason == 'long_pause')
if reason in ('denial', 'long_denial'):
self.assertFalse(sd.enabled or sd.active)
self.assertEqual(state.alertText2, 'Controls Mismatch')
self.assertEqual(state.alertSound, log.SelfdriveState.AudibleAlert.warningImmediate)
self.assertTrue(sd.aol_authority_lost)
self._assert_withdrawal_alert(controls, sd.pm.messages['selfdriveState'])
else:
self.assertNotIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
self.assertFalse(sd.aol_authority_lost)
def test_actual_eps_monitor_fault_reaches_both_axis_callers(self):
from opendbc.can import CANPacker
from opendbc.car import Bus
from opendbc.car.gm.interface import CarInterface
from opendbc.car.gm.tests.test_ascm_intercept import TestAscmIntercept
from opendbc.car.gm.values import DBC
for full_op in (False, True):
with self.subTest(full_op=full_op), OpenpilotPrefix():
sd, _, step = self._authority_callers(full_op)
ci = CarInterface(sd.CP)
packer = CANPacker(DBC[sd.CP.carFingerprint][Bus.pt])
producer = TestAscmIntercept()
for tick in range(20):
producer.authority_frame(ci, packer, tick, torque=0.)
healthy = producer.authority_frame(ci, packer, 20, torque=0.)
self.assertTrue(healthy.canValid and not healthy.canTimeout)
self.assertTrue(ci.CS.steering_authority.seen_active)
self.assertFalse(healthy.steerFaultTemporary)
self.assertTrue(step(0, observed_state=healthy)[0].lateralActive)
for tick in range(21, 30):
producer.authority_frame(ci, packer, tick, torque=0.)
loss = producer.authority_frame(ci, packer, 30, status=0, torque=0.)
self.assertTrue(loss.canValid and not loss.canTimeout)
self.assertTrue(loss.steerFaultTemporary)
axis, state, command = step(1, observed_state=loss, authority_unavailable=ci.CS.steering_authority.latched)
self.assertFalse(axis.lateralActive or command.latActive)
self.assertEqual(state.alertText2, 'Controls Mismatch')
self.assertEqual(state.alertSound, log.SelfdriveState.AudibleAlert.warningImmediate)
self.assertFalse(sd.enabled or sd.active)
self.assertIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
for tick in range(31, 40):
producer.authority_frame(ci, packer, tick, status=0, torque=0.)
recovered_status = producer.authority_frame(ci, packer, 40, torque=0.)
self.assertTrue(recovered_status.canValid and not recovered_status.canTimeout)
self.assertTrue(recovered_status.steerFaultTemporary)
self.assertFalse(step(2, observed_state=recovered_status, authority_unavailable=ci.CS.steering_authority.latched)[0].lateralActive)
# A fresh physical status2 is ordinary temporary EPS limitation, not the monitor's latched loss.
sd, _, step = self._authority_callers(full_op)
ci = CarInterface(sd.CP)
for tick in range(20):
producer.authority_frame(ci, packer, tick, torque=0.)
healthy = producer.authority_frame(ci, packer, 20, torque=0.)
self.assertTrue(healthy.canValid and not healthy.canTimeout)
self.assertTrue(step(0, observed_state=healthy)[0].lateralActive)
for tick in range(21, 30):
producer.authority_frame(ci, packer, tick, torque=0.)
temporary = producer.authority_frame(ci, packer, 30, status=2, torque=0.)
self.assertTrue(temporary.canValid and not temporary.canTimeout)
self.assertFalse(ci.CS.steering_authority.latched)
axis, state, command = step(1, observed_state=temporary)
self.assertFalse(axis.lateralActive or command.latActive)
self.assertEqual(state.alertSound, log.SelfdriveState.AudibleAlert.prompt)
self.assertNotIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
def test_car_state_timeout_retains_only_original_fresh_valid_sample(self):
sd = SelfdriveD.__new__(SelfdriveD)
sd.car_state_sock = object()
sd.CS_prev = car_state()
sd.aol_car_state_log_ns = 0
sd.conditional_car_state_valid = False
sd.initialized = True
sd.enabled = False
self.enterContext(mock.patch.object(sd, 'sm', SimpleNamespace(update=mock.Mock()), create=True))
stamp = 1_000_000_000
message = messaging.new_message('carState')
message.logMonoTime = stamp
message.valid = True
message.carState = sd.CS_prev
with (mock.patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one', return_value=message) as recv,
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=stamp) as now):
sd.CS_prev = sd.data_sample()
self.assertEqual(sd.aol_car_state_log_ns, stamp)
recv.return_value = None # 20 ms socket timeout, still inside the existing 30 ms lease.
now.return_value = stamp + 23_000_000
self.assertIs(sd.data_sample(), sd.CS_prev)
self.assertEqual(sd.aol_car_state_log_ns, stamp)
self.assertTrue(sd.conditional_car_state_valid)
now.return_value = stamp + 30_000_001
sd.data_sample()
self.assertEqual(sd.aol_car_state_log_ns, 0)
self.assertFalse(sd.conditional_car_state_valid)
# A next-drive/new process has no retained source, even if CS_prev exists.
sd.data_sample()
self.assertEqual(sd.aol_car_state_log_ns, 0)
message.logMonoTime = stamp + 24_000_000
recv.return_value = message
sd.CS_prev = sd.data_sample()
self.assertEqual(sd.aol_car_state_log_ns, stamp + 24_000_000)
for invalid_kind in ('valid', 'canValid', 'canTimeout', 'clock'):
with self.subTest(invalid_kind=invalid_kind):
message.valid = invalid_kind != 'valid'
message.carState.canValid = invalid_kind != 'canValid'
message.carState.canTimeout = invalid_kind == 'canTimeout'
recv.return_value = message
sd.CS_prev = sd.data_sample()
recv.return_value = None
now.return_value = message.logMonoTime + (-1 if invalid_kind == 'clock' else 20_000_000)
sd.data_sample()
self.assertEqual(sd.aol_car_state_log_ns, 0)
self.assertFalse(sd.conditional_car_state_valid)
def test_lateral_only_receipt_loss_keeps_take_control_alert(self):
class SM:
seen = {'aolIntentWire': False}
frame = 0
def __getitem__(self, service):
if service == 'deviceState':
return SimpleNamespace(started=True, startedMonoTime=1)
if service == 'driverMonitoringState':
return SimpleNamespace(alertLevel=0, lockout=False, alwaysOnLockout=False)
if service == 'extrinsicsCalibration':
return SimpleNamespace(calStatus=log.ExtrinsicsCalibration.Status.calibrated)
raise KeyError(service)
def all_checks(self, _services):
return True
sd = SelfdriveD.__new__(SelfdriveD)
sd.CP = car.CarParams.new_message(passive=False, openpilotLongitudinalControl=True)
self.enterContext(mock.patch.object(sd, 'sm', SM(), create=True))
sd.events = Events()
sd.state_machine = StateMachine()
sd.AM = AlertManager()
from openpilot.starpilot.controllers.mode_actions import SwitchbackCooldown
sd.switchback_capable = False
sd.switchback_cooldown = SwitchbackCooldown()
sd.switchback_setting_ns = time.monotonic_ns()
sd.switchback_cooldown_ns = 300_000_000_000
sd.personality = 1
sd.is_metric = False
sd.enabled = sd.active = False
sd.initialized = True
sd.aol_replay = True
sd.axis_transport_required = sd.aol_replay
sd.aol_car_state_log_ns = 0
sd.conditional_car_state_valid = True
sd.aol_session_id = 'drive-session'
sd.aol_axis_decision = AxisDecision()
sd.aol_dm_lateral_inhibit = False
sd.aol_authority_lost = False
sd.aol_settings = None
sd.nostalgia_paddle_cancel = False
self.enterContext(mock.patch.object(sd, 'data_sample', car_state))
self.enterContext(mock.patch.object(sd, 'update_events', lambda _cs: sd.events.clear()))
sd.update_conditional_mode = mock.Mock()
sd.publish_selfdriveState = mock.Mock()
sd.publish_axis_proof = mock.Mock()
native = SimpleNamespace(requestedLateral=True, requestedLongitudinal=False,
lateralAllowed=True, longitudinalAllowed=False)
intent_ns = time.monotonic_ns()
intent = IntentState('alert-card', 1, intent_ns, intent_ns, intent_ns + 200_000_000,
True, False, False, True, True)
with mock.patch('openpilot.selfdrive.selfdrived.selfdrived.current_intent', return_value=intent), \
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.current_native', side_effect=(None, native, None, None)):
sd.sm.frame = 1
sd.step()
self.assertEqual(sd.aol_axis_decision.mode, 'off')
self.assertFalse(sd.enabled)
self.assertNotIn(ET.IMMEDIATE_DISABLE, sd.state_machine.current_alert_types)
self.assertNotEqual(sd.AM.current_alert.alert_text_1, 'TAKE CONTROL IMMEDIATELY')
sd.sm.frame = 2
sd.step()
self.assertEqual(sd.aol_axis_decision.mode, 'lateralOnly')
self.assertFalse(sd.enabled)
sd.sm.frame = 3
sd.step()
self.assertEqual(sd.aol_axis_decision.mode, 'off')
self.assertFalse(sd.enabled)
self.assertIn(ET.IMMEDIATE_DISABLE, sd.state_machine.current_alert_types)
self.assertEqual(sd.AM.current_alert.alert_text_1, 'TAKE CONTROL IMMEDIATELY')
self.assertEqual(sd.AM.current_alert.alert_text_2, 'Controls Mismatch')
alert_end_frame = sd.AM.alerts['controlsMismatch/immediateDisable'].end_frame
self.assertGreater(alert_end_frame, sd.sm.frame)
sd.sm.frame = 4
sd.step()
self.assertNotIn(ET.IMMEDIATE_DISABLE, sd.state_machine.current_alert_types)
self.assertEqual(sd.AM.current_alert.alert_text_1, 'TAKE CONTROL IMMEDIATELY')
self.assertEqual(sd.AM.alerts['controlsMismatch/immediateDisable'].end_frame, alert_end_frame)
def test_lateral_only_temporary_fault_uses_existing_orange_warning(self):
class SM:
seen = {'aolIntentWire': False}
frame = 1
def __getitem__(self, service):
if service == 'deviceState':
return SimpleNamespace(started=True, startedMonoTime=1)
if service == 'driverMonitoringState':
return SimpleNamespace(alertLevel=0, lockout=False, alwaysOnLockout=False)
if service == 'extrinsicsCalibration':
return SimpleNamespace(calStatus=log.ExtrinsicsCalibration.Status.calibrated)
raise KeyError(service)
def all_checks(self, _services):
return True
sd = SelfdriveD.__new__(SelfdriveD)
sd.CP = car.CarParams.new_message(passive=False, openpilotLongitudinalControl=True)
self.enterContext(mock.patch.object(sd, 'sm', SM(), create=True))
sd.events, sd.state_machine, sd.AM = Events(), StateMachine(), AlertManager()
from openpilot.starpilot.controllers.mode_actions import SwitchbackCooldown
sd.switchback_capable = False
sd.switchback_cooldown = SwitchbackCooldown()
sd.switchback_setting_ns = time.monotonic_ns()
sd.switchback_cooldown_ns = 300_000_000_000
sd.personality, sd.is_metric = 1, False
sd.enabled = sd.active = False
sd.initialized = sd.aol_replay = True
sd.axis_transport_required = sd.aol_replay
sd.aol_car_state_log_ns = 0
sd.conditional_car_state_valid = True
sd.aol_session_id = 'drive-session'
sd.aol_axis_decision = AxisDecision()
sd.aol_dm_lateral_inhibit = False
sd.aol_authority_lost = False
sd.aol_settings = None
sd.nostalgia_paddle_cancel = False
state = car_state()
state.steerFaultTemporary = True
self.enterContext(mock.patch.object(sd, 'data_sample', return_value=state))
self.enterContext(mock.patch.object(sd, 'update_events', lambda _cs: sd.events.clear()))
sd.update_conditional_mode = mock.Mock()
sd.publish_selfdriveState = mock.Mock()
sd.publish_axis_proof = mock.Mock()
native = SimpleNamespace(requestedLateral=False, requestedLongitudinal=False,
lateralAllowed=False, longitudinalAllowed=False)
intent_ns = time.monotonic_ns()
intent = IntentState('alert-card', 1, intent_ns, intent_ns, intent_ns + 200_000_000,
True, False, False, True, True)
with (mock.patch('openpilot.selfdrive.selfdrived.selfdrived.current_intent', return_value=intent),
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.current_native', return_value=native)):
sd.step()
self.assertFalse(sd.enabled)
self.assertFalse(sd.aol_axis_decision.desired_lateral)
self.assertFalse(sd.aol_axis_decision.lateral_active)
self.assertEqual(sd.AM.current_alert.alert_type, 'steerTempUnavailableSilent/warning')
self.assertEqual(sd.AM.current_alert.alert_status, log.SelfdriveState.AlertStatus.userPrompt)
self.assertEqual(sd.AM.current_alert.alert_text_1, 'Steering Assist Temporarily Unavailable')
# A hard driver override puts Honda EPS in NO_TORQUE_ALERT_1; the driver
# is already steering, so the warning waits until they let go.
state.steeringPressed = True
sd.sm.frame += 1
sd.step()
self.assertNotIn(log.OnroadEvent.EventName.steerTempUnavailableSilent, sd.events.names)
state.steeringPressed = False
sd.sm.frame += 1
sd.step()
self.assertIn(log.OnroadEvent.EventName.steerTempUnavailableSilent, sd.events.names)
state.steerFaultTemporary = False
native.requestedLateral = native.lateralAllowed = True
sd.sm.frame += 1
sd.step()
self.assertTrue(sd.aol_axis_decision.lateral_active)
self.assertNotIn(log.OnroadEvent.EventName.steerTempUnavailableSilent, sd.events.names)
def test_native_bootstrap_scope_preserves_finalized_bolt_admission(self):
from opendbc.car.gm.aol import native_bootstrap_supported
from opendbc.car.gm.tests.test_bolt_cc import params as bolt_params
from opendbc.car.gm.values import CAR
from openpilot.starpilot.vehicle_preferences import VehicleStartupPreferences
for identity in (CAR.CHEVROLET_BOLT_CC_2017, CAR.CHEVROLET_BOLT_CC_2018_2021,
CAR.CHEVROLET_BOLT_CC_2022_2023, CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL):
with self.subTest(identity=identity):
cp = bolt_params(identity, present=True)
self.assertTrue(native_bootstrap_supported(cp))
removed = bolt_params(identity, present=True, removed=True)
self.assertTrue(native_bootstrap_supported(removed))
disabled = cp.as_reader().as_builder()
VehicleStartupPreferences(disable_bolt_long=True).prepare(disabled)
self.assertEqual(native_bootstrap_supported(disabled), identity != CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL)
for field, value in (('passive', True), ('notCar', True), ('dashcamOnly', True),
('carFingerprint', CAR.CHEVROLET_VOLT_CC)):
denied = cp.as_reader().as_builder()
setattr(denied, field, value)
self.assertFalse(native_bootstrap_supported(denied))
def _deferred_step(self, card, receive):
batch = []
calls = [0]
def state_update(*, defer_radar=False):
self.assertTrue(defer_radar)
calls[0] += 1
card._deferred_radar_can = batch
return receive()
def radar_update(value):
self.assertIs(value, batch)
return None
radar = self.enterContext(mock.patch.object(card.RI, 'update', side_effect=radar_update))
self.addCleanup(lambda: self.assertEqual(radar.call_count, calls[0]))
return mock.patch.object(card, 'state_update', side_effect=state_update)
def test_initialization_waits_for_current_matching_native_receipt(self):
from dataclasses import replace
from opendbc.car import gen_empty_fingerprint
from opendbc.car.gm.interface import CarInterface
from opendbc.car.gm.values import CAR
from openpilot.starpilot.aol.tests.test_gm import TestGmAol
with OpenpilotPrefix(), mock.patch.dict('os.environ', {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}):
settings = Params()
settings.put_bool('AlwaysOnLateral', True, block=True)
fingerprint = gen_empty_fingerprint()
fingerprint[0].update({0x201: 6, 0x142: 8})
fingerprint[2].update({0x320: 8, 0x180: 4})
factory_cp = CarInterface.get_params(CAR.CHEVROLET_BOLT_CC_2018_2021, fingerprint, [], True, False, False)
selected = TestGmAol.card(factory_cp, settings)
cp = selected.CP.as_reader()
self.assertFalse(settings.get_bool('ControlsReady'))
self.assertTrue(settings.get_bool('FirmwareQueryDone'))
now = 4_000_000_000
cc = car.CarControl.new_message()
cc.actuators.curvature = 0.01 # Real inactive Controls telemetry need not be zero.
boot_events = Events()
boot_events.add(log.OnroadEvent.EventName.selfdriveInitializing)
class BootstrapSM(dict):
seen = {'onroadEvents': True, 'carControl': True}
valid = {'carControl': True}
alive = {'carControl': True}
logMonoTime = {'carControl': now}
recv_time = {'carControl': now / 1e9}
def all_alive(self, _services):
return self.alive['carControl']
boot_sm = BootstrapSM(onroadEvents=boot_events.to_msg(), carControl=cc)
physical = car_state()
with (mock.patch.object(selected, 'sm', boot_sm),
self._deferred_step(selected, lambda: (physical, None)),
mock.patch.object(selected, 'state_publish'),
mock.patch.object(selected.CI, 'init', wraps=selected.CI.init) as initialize,
mock.patch.object(selected.CI, 'apply') as apply,
mock.patch.object(selected, 'publish_sendcan') as sendcan,
mock.patch('openpilot.selfdrive.car.card.time.monotonic_ns', return_value=now)):
for invalid in ('unseen', 'invalid', 'dead', 'old_source', 'future_source', 'old_receipt', 'future_receipt',
'enabled', 'lateral', 'longitudinal', 'torque', 'accel', 'nan_torque', 'bad_can', 'can_timeout'):
with self.subTest(bootstrap_invalid=invalid):
boot_sm.seen['carControl'] = invalid != 'unseen'
boot_sm.valid['carControl'] = invalid != 'invalid'
boot_sm.alive['carControl'] = invalid != 'dead'
boot_sm.logMonoTime['carControl'] = now - 150_000_001 if invalid == 'old_source' else now + 1 if invalid == 'future_source' else now
boot_sm.recv_time['carControl'] = (now - 150_000_001 if invalid == 'old_receipt' else now + 1000 if invalid == 'future_receipt' else now) / 1e9
cc.enabled, cc.latActive, cc.longActive = invalid == 'enabled', invalid == 'lateral', invalid == 'longitudinal'
cc.actuators.torque = float('nan') if invalid == 'nan_torque' else 0.1 if invalid == 'torque' else 0.0
cc.actuators.accel = 0.1 if invalid == 'accel' else 0.0
physical.canValid, physical.canTimeout = invalid != 'bad_can', invalid == 'can_timeout'
selected.step()
self.assertFalse(settings.get_bool('ControlsReady'))
self.assertFalse(selected.ci_initialized)
initialize.assert_not_called()
apply.assert_not_called()
sendcan.assert_not_called()
physical.canValid, physical.canTimeout = True, False
cc.enabled = cc.latActive = cc.longActive = False
cc.actuators.torque = cc.actuators.accel = 0.0
for excluded in ('passive', 'other_startup_owner', 'volt_transport'):
with (self.subTest(bootstrap_excluded=excluded),
mock.patch.object(selected, 'startup_panda_configured', return_value=False),
mock.patch('openpilot.selfdrive.car.card.clock_pair_ns', return_value=None)):
selected.CP.passive = excluded == 'passive'
selected.vehicle_startup.owner = mock.Mock() if excluded == 'other_startup_owner' else None
selected.volt_cc_selected = excluded == 'volt_transport'
selected.step()
self.assertFalse(settings.get_bool('ControlsReady'))
self.assertFalse(selected.ci_initialized)
initialize.assert_not_called()
apply.assert_not_called()
if excluded == 'volt_transport':
sendcan.assert_called_once_with([], valid=False)
sendcan.reset_mock()
else:
sendcan.assert_not_called()
selected.CP.passive = False
selected.vehicle_startup.owner = None
selected.volt_cc_selected = False
selected.step()
self.assertTrue(selected.ci_initialized)
self._assert_controls_ready(settings)
initialize.assert_called_once()
apply.assert_not_called()
sendcan.assert_not_called()
selected.step()
initialize.assert_called_once()
apply.assert_not_called()
sendcan.assert_not_called()
# Actual Card wrote both prerequisites consumed by PandaSafety::fetchCarParams.
with car.CarParams.from_bytes(settings.get('CarParams')) as configured_cp:
self.assertEqual(configured_cp.safetyConfigs[0].safetyParam, cp.safetyConfigs[0].safetyParam)
self.assertEqual(configured_cp.alternativeExperience, cp.alternativeExperience)
self.assertEqual((cp.safetyConfigs[0].safetyParam, cp.alternativeExperience, cp.flags), (0x9D, 32, 17))
self.assertTrue(cp.openpilotLongitudinalControl)
self.assertFalse(cp.pcmCruise)
now = 4_000_000_000
native = SafetyState(1, True, now, now + 200_000_000, int(cp.safetyConfigs[0].safetyModel.raw),
int(cp.safetyConfigs[0].safetyParam), False, False, False, False, 'panda', 'drive-session')
class SM(dict):
frame = 1
valid = {'aolSafetyWire': True, 'aolIntentWire': False}
alive = {'aolSafetyWire': True, 'aolIntentWire': False}
seen = {'aolSafetyWire': False, 'aolIntentWire': False}
logMonoTime = {'aolSafetyWire': now}
freq_ok = {'aolSafetyWire': True}
def update(self, *args, **kwargs):
pass
def all_checks(self, _services=None):
return True
for aol in (True, False):
with self.subTest(aol=aol):
sd = SelfdriveD.__new__(SelfdriveD)
sd.CP = cp
sm = SM(aolSafetyWire=b'', pandaStates=[],
driverMonitoringState=SimpleNamespace(alertLevel=0, lockout=False, alwaysOnLockout=False),
extrinsicsCalibration=SimpleNamespace(calStatus=log.ExtrinsicsCalibration.Status.calibrated))
sm.ignore_alive, sm.ignore_valid = set(), set()
sm.seen = dict(SM.seen)
self.enterContext(mock.patch.object(sd, 'sm', sm, create=True))
sd.car_state_sock = object()
sd.CS_prev = car_state()
sd.aol_car_state_log_ns = 0
sd.conditional_car_state_valid = False
sd.initialized = sd.enabled = sd.active = False
sd.aol_replay, sd.ordinary_axis_ack_required = aol, not aol
sd.axis_transport_required = True
sd.aol_session_id = 'drive-session'
sd.aol_axis_decision = AxisDecision()
sd.aol_dm_lateral_inhibit = False
sd.aol_authority_lost = False
sd.aol_settings = None
sd.nostalgia_paddle_cancel = False
sd.events, sd.state_machine = Events(), StateMachine()
sd.update_alerts = mock.Mock()
sd.update_conditional_mode = mock.Mock()
sd.publish_selfdriveState = mock.Mock()
sd.publish_axis_proof = mock.Mock()
self.enterContext(mock.patch.object(sd, 'update_events', lambda _cs, sd=sd: sd.events.clear()))
message = messaging.new_message('carState')
message.valid, message.logMonoTime, message.carState = True, now, sd.CS_prev
with (mock.patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one', return_value=message),
empty_carstate_queue(sd.car_state_sock),
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=now),
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', False),
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.SIMULATION', False),
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.VisionIpcClient',
SimpleNamespace(available_streams=lambda *_args, **_kwargs: [])),
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.cloudlog.event')):
for receipt in (None, replace(native, observedMonoTime=now - 250_000_000, validUntilMonoTime=now - 1),
replace(native, axisSessionId='old-session'), replace(native, safetyParam=native.safetyParam + 1)):
sm.seen['aolSafetyWire'] = receipt is not None
sm['aolSafetyWire'] = encode_safety(receipt) if receipt else b''
sd.step() # Actual data_sample and strict current_native decoding remain in this path.
self.assertFalse(sd.initialized)
self.assertNotIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
self.assertEqual(sd.aol_axis_decision.mode, 'off')
sm['aolSafetyWire'] = encode_safety(native)
sd.step()
self.assertTrue(sd.initialized)
self.assertNotIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
self.assertEqual(sd.aol_axis_decision.mode, 'off') # Zero-axis native acknowledgment grants no actuation.
sm.seen['aolSafetyWire'] = False
sd.step()
self.assertIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
self.assertTrue(sd.events.contains(ET.IMMEDIATE_DISABLE))
sd.initialized = False
sm.frame = 601
sd.step()
self.assertTrue(sd.initialized) # Existing six-second timeout still exposes a genuine missing receipt.
self.assertIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
def test_selected_volt_camera_bootstrap_waits_for_native(self):
self._assert_sibling_bootstrap(transport=False)
def test_selected_ordinary_cc_bootstrap_preserves_physical_clock_floor(self):
self._assert_sibling_bootstrap(transport=True)
def _assert_controls_ready(self, settings):
deadline = time.monotonic() + 1.
while not settings.get_bool('ControlsReady') and time.monotonic() < deadline:
time.sleep(.001)
self.assertTrue(settings.get_bool('ControlsReady'))
def _assert_sibling_bootstrap(self, *, transport):
from dataclasses import replace
from opendbc.can import CANPacker
from opendbc.car import Bus
from opendbc.car.gm.aol import qualified_gm
from opendbc.car.gm.ordinary_cc import control_transport_required
from opendbc.car.gm.tests.test_cc_gateway_stock import params as cc_params
from opendbc.car.gm.tests.test_ordinary_cc import qualified_frames
from opendbc.car.gm.tests.test_volt_camera_control import camera_params, feed_camera
from opendbc.car.gm.values import CAR, DBC
from openpilot.starpilot.aol.tests.test_gm import TestGmAol
factory_cp = cc_params(CAR.CADILLAC_CT6_CC) if transport else camera_params(alpha=False)
self.assertEqual(factory_cp.safetyConfigs[0].safetyParam, 0xC160 if transport else 5)
self.assertEqual(control_transport_required(factory_cp), transport)
self.assertTrue(qualified_gm(factory_cp))
now = [4_000_000_000]
offset = 4_000_000_000
pair_missing, hold_main = [False], [False]
packer = CANPacker(DBC[factory_cp.carFingerprint][Bus.pt])
cc = car.CarControl.new_message()
cc.actuators.curvature = .01
boot_events = Events()
boot_events.add(log.OnroadEvent.EventName.selfdriveInitializing)
device = messaging.new_message('deviceState').deviceState
device.started, device.startedMonoTime = True, 1_000_000_000
class BootstrapSM(dict):
def all_alive(self, services):
return all(self.alive[s] for s in services)
sm = BootstrapSM(onroadEvents=boot_events.to_msg(), carControl=cc, deviceState=device)
sm.seen = {'onroadEvents': True, 'carControl': True, 'deviceState': True}
sm.valid = {'carControl': True, 'deviceState': True}
sm.alive = dict(sm.valid)
sm.logMonoTime, sm.recv_time = {}, {}
def refresh():
# IPC receipt follows production by 1us; float seconds need not round-trip ns.
sm.logMonoTime.update(carControl=now[0] - 1_000, deviceState=now[0] - 1_000)
sm.recv_time.update(carControl=now[0] / 1e9, deviceState=now[0] / 1e9)
for service in ('carControl', 'deviceState'):
self.assertLessEqual(sm.logMonoTime[service], int(sm.recv_time[service] * 1e9))
self.assertLessEqual(int(sm.recv_time[service] * 1e9), now[0])
def parse_state(selected):
stamp = now[0] + offset
if transport:
esp = packer.make_can_msg('ESPStatus', 0, {'TractionControlOn': 1})
frames = [f for f in qualified_frames(packer, (stamp // 10_000_000) % 4) if f[0] not in (esp[0], 0x3D1)]
frames.append(esp)
frames.append(packer.make_can_msg('ECMCruiseControl', 0, {'CruiseActive': 0, 'CruiseSetSpeed': 80}))
if hold_main[0]:
frames = [f for f in frames if f[0] != 0xC9]
selected.CI.update([(stamp - 1_000_000, frames)])
physical = selected.CI.update([(stamp, frames)])
else:
physical, _ = feed_camera(selected.CI, packer, stamp, active=False)
self.assertTrue(physical.canValid)
self.assertFalse(physical.canTimeout)
self.assertFalse(physical.espDisabled)
self.assertFalse(physical.brakePressed)
self.assertEqual(physical.gearShifter, car.CarState.GearShifter.drive)
return physical, None
with OpenpilotPrefix(), mock.patch.dict('os.environ', {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}):
settings = Params()
settings.put_bool('AlwaysOnLateral', False, block=True)
normal = TestGmAol.card(factory_cp, settings)
refresh()
with (mock.patch.object(normal, 'sm', sm),
self._deferred_step(normal, lambda: parse_state(normal)),
mock.patch.object(normal, 'state_publish'),
mock.patch.object(normal.CI, 'init') as initialize,
mock.patch.object(normal.CI, 'apply') as apply):
normal.step()
self.assertFalse(normal.aol_replay)
self.assertFalse(normal.ci_initialized)
self.assertFalse(settings.get_bool('ControlsReady'))
initialize.assert_not_called()
apply.assert_not_called()
with OpenpilotPrefix(), mock.patch.dict('os.environ', {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}):
settings = Params()
settings.put_bool('AlwaysOnLateral', True, block=True)
selected = TestGmAol.card(factory_cp, settings)
self.assertTrue(selected.aol_replay)
self.assertEqual(selected.volt_cc_selected, transport)
self.assertIsNone(selected.vehicle_startup.owner)
with (mock.patch.object(selected, 'sm', sm),
self._deferred_step(selected, lambda: parse_state(selected)),
mock.patch.object(selected, 'state_publish'),
mock.patch.object(selected.CI, 'init', wraps=selected.CI.init) as initialize,
mock.patch.object(selected.CI, 'apply') as apply,
mock.patch.object(selected, 'publish_sendcan') as sendcan,
mock.patch('openpilot.selfdrive.car.card.time.monotonic_ns', side_effect=lambda: now[0]),
mock.patch('openpilot.selfdrive.car.card.clock_pair_ns',
side_effect=lambda: None if pair_missing[0] else (now[0], now[0] + offset))):
for invalid in ('enabled', 'torque', 'stale_control'):
refresh()
cc.enabled = invalid == 'enabled'
cc.actuators.torque = .1 if invalid == 'torque' else 0.
if invalid == 'stale_control':
sm.logMonoTime['carControl'] = now[0] - 150_000_001
selected.step()
self.assertFalse(settings.get_bool('ControlsReady'))
initialize.assert_not_called()
apply.assert_not_called()
cc.enabled, cc.actuators.torque = False, 0.
refresh()
if transport:
selected.step() # Actual paired-clock acquisition establishes the source floor.
self.assertEqual(selected.volt_cc_source_floor_ns, now[0])
self.assertFalse(settings.get_bool('ControlsReady'))
now[0] += 10_000_000
refresh()
hold_main[0] = True
selected.step() # C9 still at the real floor; all other physical sources are refreshed.
self.assertFalse(settings.get_bool('ControlsReady'))
self.assertLessEqual(selected.CI.CS.volt_cc_physical.source_ns[2] - offset, selected.volt_cc_source_floor_ns)
hold_main[0] = False
device.started = False
selected.step()
self.assertFalse(settings.get_bool('ControlsReady'))
device.started = True
sm.logMonoTime['deviceState'] = now[0] - 2_000_000_001
selected.step()
self.assertFalse(settings.get_bool('ControlsReady'))
refresh()
pair_missing[0] = True
selected.step()
self.assertFalse(settings.get_bool('ControlsReady'))
pair_missing[0] = False
selected.step() # Recovery acquires a new floor; cannot admit same-frame sources.
self.assertFalse(settings.get_bool('ControlsReady'))
now[0] += 10_000_000
refresh()
initialize.assert_not_called()
self.assertTrue(sendcan.call_args_list)
self.assertTrue(all(c.args == ([],) and c.kwargs == {'valid': False} for c in sendcan.call_args_list))
selected.step()
self.assertTrue(selected.ci_initialized)
self._assert_controls_ready(settings)
initialize.assert_called_once()
apply.assert_not_called()
self.assertTrue(all(c.args == ([],) and c.kwargs == {'valid': False} for c in sendcan.call_args_list))
selected.step()
initialize.assert_called_once()
apply.assert_not_called()
cp = selected.CP.as_reader()
with car.CarParams.from_bytes(settings.get('CarParams')) as configured:
self.assertEqual(configured.safetyConfigs[0].safetyParam, cp.safetyConfigs[0].safetyParam)
self.assertEqual(configured.alternativeExperience, cp.alternativeExperience)
physical = selected.CS_prev
receipt_now = now[0]
# Synthetic zero-axis Panda configuration receipt is withheld until actual Card wrote ControlsReady.
native = SafetyState(1, True, receipt_now, receipt_now + 200_000_000, int(cp.safetyConfigs[0].safetyModel.raw),
int(cp.safetyConfigs[0].safetyParam), False, False, False, False, 'panda', 'drive-session')
class SM(dict):
frame = 1
valid = {'aolSafetyWire': True, 'aolIntentWire': False}
alive = {'aolSafetyWire': True, 'aolIntentWire': False}
seen = {'aolSafetyWire': False, 'aolIntentWire': False}
logMonoTime = {'aolSafetyWire': receipt_now}
freq_ok = {'aolSafetyWire': True}
def update(self, *args, **kwargs):
pass
def all_checks(self, _services=None):
return True
for aol in (True,):
with self.subTest(aol=aol):
sd = SelfdriveD.__new__(SelfdriveD)
sd.CP = cp
sm = SM(aolSafetyWire=b'', pandaStates=[],
driverMonitoringState=SimpleNamespace(alertLevel=0, lockout=False, alwaysOnLockout=False),
extrinsicsCalibration=SimpleNamespace(calStatus=log.ExtrinsicsCalibration.Status.calibrated))
sm.ignore_alive, sm.ignore_valid = set(), set()
sm.seen = dict(SM.seen)
self.enterContext(mock.patch.object(sd, 'sm', sm, create=True))
sd.car_state_sock = object()
sd.CS_prev = physical
sd.aol_car_state_log_ns = 0
sd.conditional_car_state_valid = False
sd.initialized = sd.enabled = sd.active = False
sd.aol_replay, sd.ordinary_axis_ack_required = aol, not aol
sd.axis_transport_required = True
sd.aol_session_id = 'drive-session'
sd.aol_axis_decision = AxisDecision()
sd.aol_dm_lateral_inhibit = False
sd.aol_authority_lost = False
sd.aol_settings = None
sd.nostalgia_paddle_cancel = False
sd.events, sd.state_machine = Events(), StateMachine()
sd.update_alerts = mock.Mock()
sd.update_conditional_mode = mock.Mock()
sd.publish_selfdriveState = mock.Mock()
sd.publish_axis_proof = mock.Mock()
self.enterContext(mock.patch.object(sd, 'update_events', lambda _cs, sd=sd: sd.events.clear()))
message = messaging.new_message('carState')
message.valid, message.logMonoTime, message.carState = True, receipt_now, sd.CS_prev
with (mock.patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one', return_value=message),
empty_carstate_queue(sd.car_state_sock),
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=receipt_now),
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', False),
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.SIMULATION', False),
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.VisionIpcClient',
SimpleNamespace(available_streams=lambda *_args, **_kwargs: [])),
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.cloudlog.event')):
for receipt in (None, replace(native, observedMonoTime=receipt_now - 250_000_000, validUntilMonoTime=receipt_now - 1),
replace(native, axisSessionId='old-session'), replace(native, safetyParam=native.safetyParam + 1)):
sm.seen['aolSafetyWire'] = receipt is not None
sm['aolSafetyWire'] = encode_safety(receipt) if receipt else b''
sd.step() # Actual data_sample and strict current_native decoding remain in this path.
self.assertFalse(sd.initialized)
self.assertNotIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
self.assertEqual(sd.aol_axis_decision.mode, 'off')
sm['aolSafetyWire'] = encode_safety(native)
sd.step()
self.assertTrue(sd.initialized)
self.assertNotIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
self.assertEqual(sd.aol_axis_decision.mode, 'off') # Zero-axis native acknowledgment grants no actuation.
sm.seen['aolSafetyWire'] = False
sd.step()
self.assertIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
self.assertTrue(sd.events.contains(ET.IMMEDIATE_DISABLE))
sd.initialized = False
sm.frame = 601
sd.step()
self.assertTrue(sd.initialized) # Existing six-second timeout still exposes a genuine missing receipt.
self.assertIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
def test_native_receipt_gates_engagement_and_disables_on_loss(self):
class SM:
seen = {'aolIntentWire': False}
def __getitem__(self, service):
if service == 'deviceState':
return SimpleNamespace(started=True, startedMonoTime=1)
if service == 'driverMonitoringState':
return SimpleNamespace(alertLevel=0, lockout=False, alwaysOnLockout=False)
if service == 'extrinsicsCalibration':
return SimpleNamespace(calStatus=log.ExtrinsicsCalibration.Status.calibrated)
raise KeyError(service)
def all_checks(self, _services):
return True
def driver(aol_enabled):
sd = SelfdriveD.__new__(SelfdriveD)
sd.CP = car.CarParams.new_message(passive=False, openpilotLongitudinalControl=True)
self.enterContext(mock.patch.object(sd, 'sm', SM(), create=True))
sd.events = Events()
sd.state_machine = StateMachine()
sd.enabled = sd.active = False
sd.initialized = True
sd.aol_replay = aol_enabled
sd.axis_transport_required = sd.aol_replay
sd.aol_car_state_log_ns = 0
sd.conditional_car_state_valid = True
sd.aol_session_id = 'drive-session'
sd.aol_axis_decision = AxisDecision()
sd.aol_dm_lateral_inhibit = False
sd.aol_authority_lost = False
sd.aol_settings = None
sd.nostalgia_paddle_cancel = False
self.enterContext(mock.patch.object(sd, 'data_sample', car_state))
sd.update_alerts = mock.Mock()
sd.update_conditional_mode = mock.Mock()
sd.publish_selfdriveState = mock.Mock()
sd.publish_axis_proof = mock.Mock()
self.enterContext(mock.patch.object(sd, 'update_events',
lambda _cs: (sd.events.clear(), sd.events.add(log.OnroadEvent.EventName.buttonEnable))))
return sd
native = SimpleNamespace(requestedLateral=True, requestedLongitudinal=True,
lateralAllowed=True, longitudinalAllowed=True)
intent_ns = time.monotonic_ns()
intent = IntentState('alert-card', 1, intent_ns, intent_ns, intent_ns + 200_000_000,
True, False, False, True, True)
sd = driver(True)
sd.initialized = False
with mock.patch('openpilot.selfdrive.selfdrived.selfdrived.current_intent', return_value=intent), \
mock.patch('openpilot.selfdrive.selfdrived.selfdrived.current_native', side_effect=(None, None, native, None)):
sd.step()
self.assertFalse(sd.enabled or sd.active)
self.assertEqual(sd.aol_axis_decision.mode, 'off')
self.assertNotIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
self.assertFalse(disarming_fault(sd.events.to_msg(), car_state()))
sd.publish_selfdriveState.assert_called_once()
sd.publish_selfdriveState.reset_mock()
sd.initialized = True
sd.step()
self.assertFalse(sd.enabled)
self.assertTrue(sd.events.contains(ET.NO_ENTRY))
self.assertEqual(sd.aol_axis_decision.mode, 'off')
sd.publish_selfdriveState.assert_called_once() # disabled bootstrap still publishes the zero-axis request
sd.step()
self.assertTrue(sd.enabled and sd.active)
self.assertEqual(sd.aol_axis_decision.mode, 'combined')
sd.step()
self.assertFalse(sd.enabled or sd.active)
self.assertTrue(sd.events.contains(ET.IMMEDIATE_DISABLE))
alerts = sd.events.create_alerts(sd.state_machine.current_alert_types)
self.assertTrue(any(alert.alert_text_1 == 'TAKE CONTROL IMMEDIATELY' and
alert.alert_text_2 == 'Controls Mismatch' for alert in alerts))
stock = driver(False)
with mock.patch('openpilot.selfdrive.selfdrived.selfdrived.current_native') as read_native:
stock.step()
read_native.assert_not_called()
self.assertTrue(stock.enabled and stock.active)
def test_independent_ack_keeps_unchanged_axis(self):
state = car_state()
old_combined = SimpleNamespace(requestedLateral=True, requestedLongitudinal=True,
lateralAllowed=True, longitudinalAllowed=True)
intent = SimpleNamespace(allowedLatch=True, lateralArmed=True, pauseLateral=False, pauseLongitudinal=False)
def decide(standard_long, native):
return decide_axes(standard_lateral=False, standard_longitudinal=standard_long,
intent=intent, native=native, car_state=state, initialized=True,
model_ready=True, no_entry=False, immediate_disable=False,
dm_lockout=False, pause_brake_mps=5.0)
self.assertEqual(decide(True, old_combined).mode, 'combined')
self.assertEqual(decide(False, old_combined).mode, 'lateralOnly')
old_lateral = SimpleNamespace(requestedLateral=True, requestedLongitudinal=False,
lateralAllowed=True, longitudinalAllowed=False)
self.assertEqual(decide(True, old_lateral).mode, 'lateralOnly')
intent.pauseLateral = True
self.assertEqual(decide(True, old_combined).mode, 'longitudinalOnly')
intent.pauseLateral = False
intent.pauseLongitudinal = True
self.assertEqual(decide(True, old_combined).mode, 'lateralOnly')
intent.pauseLongitudinal = False
old_long = SimpleNamespace(requestedLateral=False, requestedLongitudinal=True,
lateralAllowed=False, longitudinalAllowed=True)
self.assertEqual(decide(True, old_long).mode, 'longitudinalOnly')
def test_card_selfdrive_panda_cadence_and_four_modes(self):
with OpenpilotPrefix():
messaging.reset_context()
pm = messaging.PubMaster(['carState', 'aolIntentWire', 'aolAxisState', 'aolSafetyWire'])
sm = messaging.SubMaster(['carState', 'aolIntentWire', 'aolAxisState', 'aolSafetyWire'])
base = time.monotonic_ns() - 50_000_000
from opendbc.car import gen_empty_fingerprint
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.values import CAR
from openpilot.starpilot.aol.vehicle import policy_for
CP = CarInterface.get_params(CAR.HONDA_ACCORD, gen_empty_fingerprint(), [], True, False, False)
policy = policy_for(CP)
CP.alternativeExperience |= policy.alternative_experience_addition
CP.safetyConfigs[-1].safetyParam |= policy.safety_param_addition
self.assertEqual(CP.safetyConfigs[-1].safetyParam, 34)
def publish_intent(stamp, *, pause_lat=False, pause_long=False):
message = messaging.new_message('aolIntentWire', 0)
message.logMonoTime = stamp
message.valid = True
message.aolIntentWire = encode_intent(IntentState(
'card-session', stamp - base + 1, stamp, stamp, stamp + 100_000_000,
True, pause_lat, pause_long, True))
pm.send('aolIntentWire', message)
def publish_native(stamp, lat, long, session='drive-session'):
message = messaging.new_message('aolSafetyWire', 0)
message.logMonoTime = stamp
message.valid = True
message.aolSafetyWire = encode_safety(SafetyState(
1, True, stamp, stamp + 200_000_000, int(car.CarParams.SafetyModel.hondaBosch),
34, lat, long, lat, long, 'panda', session))
pm.send('aolSafetyWire', message)
for index, (standard_long, pause_lat, pause_long, expected) in enumerate((
(False, False, False, 'lateralOnly'),
(True, False, False, 'combined'),
(True, True, False, 'longitudinalOnly'),
(False, True, False, 'off'),
)):
stamp = base + index * 10_000_000
state_msg = messaging.new_message('carState')
state_msg.logMonoTime = stamp
state_msg.valid = True
pm.send('carState', state_msg)
publish_intent(stamp, pause_lat=pause_lat, pause_long=pause_long)
desired_lat = not pause_lat
desired_long = standard_long and not pause_long
publish_native(stamp, desired_lat, desired_long)
sm.update(100)
intent = current_intent(sm, car_state_ns=stamp, now_ns=stamp + 1_000_000)
native = current_native(sm, CP, now_ns=stamp + 1_000_000, axis_session_id='drive-session')
decision = decide_axes(standard_lateral=False, standard_longitudinal=standard_long,
intent=intent, native=native, car_state=car_state(),
initialized=True, model_ready=True, no_entry=False,
immediate_disable=False, dm_lockout=False, pause_brake_mps=5.0)
self.assertEqual(decision.mode, expected)
self.assertIsNone(current_native(sm, CP, now_ns=base + 500_000_000, axis_session_id='drive-session'))
self.assertIsNone(current_native(sm, CP, now_ns=base + 31_000_000, axis_session_id='new-session'))
def test_no_native_ack_still_requests_but_does_not_actuate(self):
intent = SimpleNamespace(pauseLateral=False, pauseLongitudinal=False, allowedLatch=True)
decision = decide_axes(standard_lateral=False, standard_longitudinal=True,
intent=intent, native=None, car_state=car_state(), initialized=True,
model_ready=True, no_entry=False, immediate_disable=False,
dm_lockout=False, pause_brake_mps=5.0)
self.assertTrue(decision.desired_lateral)
self.assertTrue(decision.desired_longitudinal)
self.assertEqual(decision.mode, 'off')
self.assertFalse(decision.native_acknowledged)
class UiProcessIntentTests(unittest.TestCase):
def make_card(self):
from openpilot.selfdrive.car.card import Car
from openpilot.starpilot.aol.intent import AolProcessFaultContext
from openpilot.starpilot.aol.tests.test_ioniq6_stock_repair import stock
card = Car.__new__(Car)
card.CP = stock(True)
card.aol_card_intent = None # Real Card initializes this when no optional owner is selected.
card.aol_process_fault_context = AolProcessFaultContext()
class SM:
seen = {'managerState': True}
valid = {'managerState': True}
alive = {'managerState': True}
logMonoTime = {'managerState': 1_000_000_000}
events = Events()
processes = []
def __getitem__(self, key):
if key == 'managerState':
return SimpleNamespace(processes=self.processes)
if key == 'onroadEvents':
return self.events.to_msg()
raise KeyError(key)
self.enterContext(mock.patch.object(card, 'sm', SM(), create=True))
return card
def observe(self, card, stamp, failed):
card.sm.logMonoTime['managerState'] = stamp
card.sm.processes = [SimpleNamespace(name=name, running=name not in failed, shouldBeRunning=True)
for name in ('ui', 'modeld', 'controlsd', 'card', 'pandad', 'unknown_service')]
card.aol_process_fault_context.observe(card.sm, stamp + 1)
def test_actual_card_ui_fault_pause_recovery_and_ordinary_cancel_handoff(self):
from openpilot.starpilot.aol.tests.test_ioniq6_stock_repair import stock
from openpilot.starpilot.car.hyundai.aol import create_intent, native_latch_rejected
card = self.make_card()
cp = stock(True)
cp.openpilotLongitudinalControl, cp.pcmCruise = True, False
cp.safetyConfigs[0].safetyParam = 0x8895
card.CP = cp
owner = create_intent(cp, AolSettings(True, 0., 9, 0, (0, 0, 0), (0, 0, 0)))
state = car_state()
state.cruiseState.available = True
owner.update(state, now_ns=100)
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=True)]
owner.update(state, now_ns=200)
state.buttonEvents = []
self.assertTrue(owner.allowed_latch)
self.observe(card, 1_000_000_000, {'ui'})
card.sm.events.add(log.OnroadEvent.EventName.processNotRunning)
event_ns = 1_020_000_000
self.assertTrue(card.sm.events.contains(ET.NO_ENTRY))
self.assertTrue(card.sm.events.contains(ET.SOFT_DISABLE))
fault = card.aol_disarming_fault(state, event_ns, event_ns + 1)
self.assertFalse(fault)
owner.update(state, fault_active=fault)
self.assertTrue(owner.allowed_latch)
native = SimpleNamespace(requestedLateral=False, requestedLongitudinal=False,
lateralAllowed=False, longitudinalAllowed=False)
self.assertFalse(native_latch_rejected(cp, native))
for standard in (True, False):
paused = decide_axes(standard_lateral=standard, standard_longitudinal=standard, intent=SimpleNamespace(
allowedLatch=owner.allowed_latch, pauseLateral=False, pauseLongitudinal=False), native=native,
car_state=state, initialized=True, model_ready=True, no_entry=True, immediate_disable=False,
dm_lockout=False, pause_brake_mps=0)
self.assertTrue(owner.allowed_latch)
self.assertEqual(paused.mode, 'off')
self.assertFalse(paused.desired_lateral or paused.desired_longitudinal)
# Manager recovery precedes clearing the old fault event, as in the route.
self.observe(card, 1_400_000_000, set())
self.assertFalse(card.aol_disarming_fault(state, event_ns, 1_410_000_000))
card.sm.events.clear()
owner.update(state, fault_active=card.aol_disarming_fault(state, 1_420_000_000, 1_420_000_001))
native.requestedLateral = native.lateralAllowed = True
recovered = decide_axes(standard_lateral=False, standard_longitudinal=False,
intent=SimpleNamespace(allowedLatch=owner.allowed_latch, pauseLateral=False, pauseLongitudinal=False),
native=native, car_state=state, initialized=True, model_ready=True, no_entry=False,
immediate_disable=False, dm_lockout=False, pause_brake_mps=0)
self.assertEqual(recovered.mode, 'lateralOnly') # No normal engagement or fresh button required.
self.assertTrue(recovered.desired_lateral)
self.assertFalse(recovered.desired_longitudinal)
native.requestedLongitudinal = native.longitudinalAllowed = True
for standard, expected in ((True, 'combined'), (False, 'lateralOnly')):
state.buttonEvents = ([] if standard else
[car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.altButton2, pressed=True)])
owner.update(state, standard_enabled=standard)
self.assertTrue(owner.allowed_latch)
native.requestedLongitudinal = native.longitudinalAllowed = standard
decision = decide_axes(standard_lateral=standard, standard_longitudinal=standard,
intent=SimpleNamespace(allowedLatch=owner.allowed_latch, pauseLateral=False, pauseLongitudinal=False),
native=native, car_state=state, initialized=True, model_ready=True, no_entry=False,
immediate_disable=False, dm_lockout=False, pause_brake_mps=0)
self.assertEqual(decision.mode, expected)
# Real requested-lateral revocation and critical faults keep their authority.
native.lateralAllowed = False
self.assertTrue(native_latch_rejected(cp, native))
owner.update(state, now_ns=2_000_000_000, native_rejection_ns=1_990_000_000)
self.assertFalse(owner.allowed_latch)
owner.allowed_latch = True
from dataclasses import replace
owner.settings = replace(owner.settings, cancel_actions=(3, 0, 0))
state.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.cancel, pressed=True)]
owner.update(state)
self.assertFalse(owner.allowed_latch) # Physical CANCEL remains intentional withdrawal.
def test_soft_process_context_guards_remain_distinct_from_immediate_faults(self):
card = self.make_card()
state = car_state()
card.sm.events.add(log.OnroadEvent.EventName.processNotRunning)
event_ns = 1_020_000_000
self.assertFalse(card.aol_disarming_fault(state, event_ns, event_ns + 1))
self.assertFalse(card.aol_process_fault_context.temporary_ui_failure(event_ns, event_ns + 1))
for failures in ({'controlsd'}, {'unknown_service'}, {'ui', 'modeld'}, set()):
with self.subTest(failures=failures):
card.aol_process_fault_context.history.clear()
self.observe(card, 1_000_000_000, failures)
self.assertFalse(card.aol_disarming_fault(state, event_ns, event_ns + 1))
self.assertFalse(card.aol_process_fault_context.temporary_ui_failure(event_ns, event_ns + 1))
self.observe(card, 1_100_000_000, {'ui'})
self.assertFalse(card.aol_disarming_fault(state, 1_120_000_000, 2_600_000_001))
self.assertFalse(card.aol_process_fault_context.temporary_ui_failure(1_120_000_000, 2_600_000_001))
self.observe(card, 1_150_000_000, {'ui', 'modeld'})
self.assertFalse(card.aol_disarming_fault(state, 1_120_000_000, 1_160_000_000))
self.assertFalse(card.aol_process_fault_context.temporary_ui_failure(1_120_000_000, 1_160_000_000))
card.aol_process_fault_context.history.clear()
self.observe(card, 1_100_000_000, {'ui'})
card.sm.processes[0].shouldBeRunning = False
card.aol_process_fault_context.observe(card.sm, 1_120_000_000)
self.assertFalse(card.aol_disarming_fault(state, 1_120_000_000, 1_120_000_001))
self.assertFalse(card.aol_process_fault_context.temporary_ui_failure(1_120_000_000, 1_120_000_001))
self.observe(card, 1_100_000_000, {'ui'})
for source_flag in ('seen', 'valid', 'alive'):
with self.subTest(source_flag=source_flag):
getattr(card.sm, source_flag)['managerState'] = False
card.aol_process_fault_context.observe(card.sm, 1_120_000_000)
self.assertFalse(card.aol_disarming_fault(state, 1_120_000_000, 1_120_000_001))
self.assertFalse(card.aol_process_fault_context.temporary_ui_failure(1_120_000_000, 1_120_000_001))
getattr(card.sm, source_flag)['managerState'] = True
self.observe(card, 1_100_000_000, {'ui'})
for name in (log.OnroadEvent.EventName.controlsMismatch, log.OnroadEvent.EventName.steerUnavailable):
with self.subTest(name=name):
card.sm.events.add(name)
self.assertTrue(card.aol_disarming_fault(state, 1_120_000_000, 1_120_000_001))
card.sm.events.clear()
card.sm.events.add(log.OnroadEvent.EventName.processNotRunning)
state.steerFaultPermanent = True
self.assertTrue(card.aol_disarming_fault(state, 1_120_000_000, 1_120_000_001))
if __name__ == '__main__':
unittest.main()