mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
be8cf95de8
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)
2193 lines
117 KiB
Python
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()
|