mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
766 lines
45 KiB
Python
766 lines
45 KiB
Python
import os
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
from openpilot.cereal import log, messaging
|
|
from openpilot.common.params import Params
|
|
from openpilot.common.prefix import OpenpilotPrefix
|
|
from opendbc.car.structs import car
|
|
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.runtime import AxisDecision, current_native
|
|
from openpilot.starpilot.aol.wire import IntentState, PandaSlot, SafetyState, encode_intent, encode_safety
|
|
|
|
|
|
def car_state(*, brake=False):
|
|
return car.CarState(canValid=True, gearShifter=car.CarState.GearShifter.drive,
|
|
vEgo=20.0, brakePressed=brake)
|
|
|
|
|
|
def assert_ordinary_source_pause(sd, *, publication_only=False):
|
|
"""Host loss withdraws commands while the existing state machine owns life."""
|
|
assert sd.enabled and sd.active
|
|
if publication_only:
|
|
assert sd.state_machine.state == log.SelfdriveState.OpenpilotState.enabled
|
|
assert sd.state_machine.soft_disable_timer == 0
|
|
else:
|
|
assert sd.state_machine.state == log.SelfdriveState.OpenpilotState.softDisabling
|
|
assert 0 < sd.state_machine.soft_disable_timer <= 300
|
|
assert log.OnroadEvent.EventName.commIssue in sd.events.names
|
|
assert log.OnroadEvent.EventName.controlsMismatch not in sd.events.names
|
|
assert not sd.events.contains(ET.ENABLE)
|
|
assert not sd.aol_authority_lost and not getattr(sd, 'aol_availability_long_inhibit', False)
|
|
assert sd.aol_fault_reason == 'availabilityPause'
|
|
assert sd.aol_car_state_log_ns == 0 and not sd.conditional_car_state_valid
|
|
assert not sd.aol_axis_decision.lateral_active and not sd.aol_axis_decision.longitudinal_active
|
|
|
|
|
|
def assert_fresh_ordinary_recovery(sd, axis, command, source):
|
|
"""Only a fresh exact input plus its current native grant resumes commands."""
|
|
assert sd.enabled and sd.active
|
|
assert sd.state_machine.state == log.SelfdriveState.OpenpilotState.enabled
|
|
assert axis.sourceCarStateMonoTime == source
|
|
assert 0 <= axis.observedMonoTime - source <= 30_000_000
|
|
assert axis.validUntilMonoTime == axis.observedMonoTime + 30_000_000
|
|
assert axis.longitudinalActive and command.longActive
|
|
native = current_native(sd.sm, sd.CP, now_ns=axis.observedMonoTime, axis_session_id=sd.aol_session_id)
|
|
assert native is not None and native.requestedLongitudinal and native.longitudinalAllowed
|
|
assert not sd.events.contains(ET.ENABLE)
|
|
assert not sd.aol_authority_lost and not getattr(sd, 'aol_availability_long_inhibit', False)
|
|
|
|
|
|
class IoniqAvailabilityTests(unittest.TestCase):
|
|
def _authority_callers(self, full_op, *, cp=None, intent_companion=None, genuine_events=False, initial_panda_config=None):
|
|
self.enterContext(mock.patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', False))
|
|
from opendbc.car.gm.tests.test_bolt_volt_configurations import ordinary_params
|
|
from opendbc.car.gm.values import CAR
|
|
from openpilot.selfdrive.controls.controlsd import Controls
|
|
from openpilot.starpilot.aol.vehicle import policy_for
|
|
from openpilot.starpilot.controllers.mode_actions import SwitchbackCooldown
|
|
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
|
|
|
|
params = Params()
|
|
params.put_bool('OpenpilotEnabledToggle', True, block=True)
|
|
params.put_bool('SafeMode', False, block=True)
|
|
params.put_bool('AlwaysOnLateral', True, block=True)
|
|
supplied_cp = cp is not None
|
|
if cp is None:
|
|
cp = ordinary_params(CAR.CHEVROLET_VOLT_ASCM, alpha=True, sascm=True, radar=True)
|
|
policy = policy_for(cp)
|
|
cp.safetyConfigs[-1].safetyParam |= policy.safety_param_addition
|
|
cp.alternativeExperience |= policy.alternative_experience_addition
|
|
params.put('CarParams', cp.to_bytes(), block=True)
|
|
controls = Controls()
|
|
sd = SelfdriveD(CP=cp.as_reader()) if supplied_cp else SelfdriveD.__new__(SelfdriveD)
|
|
sd.params = params
|
|
sd.CP = cp.as_reader()
|
|
services = ['deviceState', 'pandaStates', 'driverMonitoringState', 'extrinsicsCalibration', 'modelV2',
|
|
'carState', 'carControl', 'aolIntentWire', 'aolSafetyWire', 'starpilotCarState']
|
|
if not genuine_events:
|
|
self.enterContext(mock.patch.object(sd, 'sm', messaging.SubMaster(services, ignore_avg_freq=services), create=True))
|
|
sd.sm.update = mock.Mock()
|
|
# Preserve the real constructor's CP-qualified family event rules.
|
|
if not (supplied_cp and genuine_events):
|
|
sd.events = Events()
|
|
sd.state_machine, sd.AM = StateMachine(), AlertManager()
|
|
sd.state_machine.state = log.SelfdriveState.OpenpilotState.enabled if full_op else log.SelfdriveState.OpenpilotState.disabled
|
|
sd.enabled = sd.active = full_op
|
|
sd.mismatch_counter = 0
|
|
sd.events_prev = []
|
|
sd.experimental_mode = sd.requested_experimental_mode = False
|
|
sd.initialized = sd.aol_replay = True
|
|
sd.axis_transport_required = sd.aol_replay or policy.ordinary_axis_ack_required
|
|
sd.aol_authority_lost = sd.aol_dm_lateral_inhibit = False
|
|
sd.aol_axis_decision = AxisDecision()
|
|
sd.aol_settings = None
|
|
sd.aol_car_state_log_ns = 0
|
|
sd.conditional_car_state_valid = False
|
|
sd.aol_last_intent = None
|
|
if intent_companion is not None or not supplied_cp:
|
|
sd.aol_intent_companion = intent_companion
|
|
sd.aol_session_id, sd.aol_sequence = 'authority-session', 0
|
|
from openpilot.starpilot.aol.transport_pause import TransportPause
|
|
sd.aol_transport_pause = TransportPause(sd.CP, sd.aol_session_id)
|
|
sd.conditional_replay = False
|
|
sd.nostalgia_paddle_cancel = False
|
|
sd.switchback_capable = False
|
|
sd.switchback_cooldown = SwitchbackCooldown()
|
|
sd.switchback_setting_ns, sd.switchback_cooldown_ns = 0, 300_000_000_000
|
|
sd.personality, sd.is_metric = 1, False
|
|
sd.CS_prev = car_state()
|
|
sd.car_state_sock = object()
|
|
original_optional_receive = messaging.recv_one_or_none
|
|
steering_receipts = []
|
|
|
|
def optional_receive(sock):
|
|
if getattr(sd, 'steering_companion', None) is not None and sock is sd.steering_companion.sock:
|
|
return steering_receipts.pop(0) if steering_receipts else None
|
|
return None if sock is sd.car_state_sock else original_optional_receive(sock)
|
|
|
|
self.enterContext(mock.patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one_or_none', side_effect=optional_receive))
|
|
sd.update_conditional_mode = mock.Mock()
|
|
if not genuine_events:
|
|
self.enterContext(mock.patch.object(sd, 'update_events', lambda cs: (sd.events.clear(), sd._update_steering_authority(cs))))
|
|
else:
|
|
self.assertTrue(supplied_cp)
|
|
self.assertFalse(sd.sm.simulation)
|
|
sd.startup_event = None
|
|
# Retain the real 100 Hz Ratekeeper created by SelfdriveD(CP=...).
|
|
|
|
class Publisher:
|
|
def __init__(self):
|
|
self.messages = {}
|
|
def send(self, service, message):
|
|
self.messages[service] = messaging.log_from_bytes(message.to_bytes())
|
|
self.enterContext(mock.patch.object(sd, 'pm', Publisher(), create=True))
|
|
self.enterContext(mock.patch.object(controls, 'pm', Publisher()))
|
|
clock = self.enterContext(mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=10_000_000_000))
|
|
recv = self.enterContext(mock.patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one'))
|
|
base = 10_000_000_000
|
|
seconds = (self.enterContext(mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic',
|
|
return_value=base / 1e9)) if genuine_events else None)
|
|
original_message = messaging.new_message
|
|
def clocked_message(*args, **kwargs):
|
|
message = original_message(*args, **kwargs)
|
|
if 'logMonoTime' not in kwargs:
|
|
message.logMonoTime = clock.return_value
|
|
return message
|
|
self.enterContext(mock.patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.new_message', clocked_message))
|
|
|
|
|
|
# The opt-in path keeps the constructor's production subscriptions and
|
|
# frequency/validity checks. Only receive transport and the clock are synthetic.
|
|
previous_control = [car.CarControl(enabled=full_op, latActive=full_op, longActive=full_op).as_reader()]
|
|
# Production subscribes before the first Controls packet arrives. Preserve
|
|
# that unseen state, then feed only the preceding actual published command.
|
|
previous_control_message = [None]
|
|
due = {}
|
|
controls_panda_due = [base]
|
|
def feed_event_sources(stamp, companions=(), *, car_output_valid=True):
|
|
from openpilot.cereal.services import SERVICE_LIST
|
|
names = ['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'extrinsicsCalibration',
|
|
'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'deviceMotion', 'lateralDelay',
|
|
'managerState', 'vehicleParameters', 'radarState', 'lateralTorqueParameters',
|
|
'controlsState', 'carControl', 'driverAssistance', *sd.camera_packets, *sd.sensor_packets]
|
|
supplied = {sample.which(): sample for sample in companions}
|
|
samples = []
|
|
for name in dict.fromkeys([*names, *supplied]):
|
|
frequency = SERVICE_LIST[name].frequency
|
|
# Ignored authority wires remain fresh on every 100 Hz source tick.
|
|
period = round(1e9 / min(frequency, 100)) if frequency > 0 else 0
|
|
if (stamp < due.get(name, 0) and name not in ('aolIntentWire', 'aolSafetyWire', 'starpilotCarState') and
|
|
not (name == 'pandaStates' and initial_panda_config is None and name in supplied and
|
|
any(p.safetyRxChecksInvalid or p.faults for p in supplied[name].pandaStates))):
|
|
continue
|
|
due[name] = stamp + period
|
|
sample = supplied.get(name)
|
|
if sample is None:
|
|
sample = messaging.new_message(name, len(cp.safetyConfigs) if name == 'pandaStates' else None,
|
|
valid=name != 'carOutput' or car_output_valid, logMonoTime=stamp)
|
|
value = getattr(sample, name)
|
|
if name == 'deviceState':
|
|
value.started, value.startedMonoTime, value.freeSpacePercent = True, base - 1_000_000_000, 50.
|
|
elif name == 'pandaStates':
|
|
for panda, config in zip(value, cp.safetyConfigs, strict=True):
|
|
if initial_panda_config is None:
|
|
panda.safetyModel, panda.safetyParam = config.safetyModel, config.safetyParam
|
|
panda.alternativeExperience, panda.controlsAllowed = cp.alternativeExperience, full_op
|
|
else:
|
|
model, word, experience, controls_allowed, rx_invalid = initial_panda_config
|
|
panda.safetyModel, panda.safetyParam, panda.alternativeExperience = model, word, experience
|
|
panda.controlsAllowed, panda.safetyRxChecksInvalid = controls_allowed, rx_invalid
|
|
elif name == 'extrinsicsCalibration':
|
|
value.calStatus, value.rpyCalib = 'calibrated', [0., 0., 0.]
|
|
elif name == 'deviceMotion':
|
|
value.inputsOK = value.posenetOK = True
|
|
elif name == 'vehicleParameters':
|
|
value.valid = True
|
|
elif name == 'controlsState':
|
|
value.lateralControlState.init('torqueState')
|
|
elif name == 'carControl':
|
|
sample.carControl = previous_control[0]
|
|
sample = sample.as_reader()
|
|
samples.append(sample)
|
|
sd.sm.update_msgs(stamp / 1e9, samples)
|
|
|
|
if genuine_events:
|
|
# Prime actual cadence trackers before the initialized onroad baseline;
|
|
# this advances fixture time only and neither sleeps nor skips a check.
|
|
for prime in range(-110, 0):
|
|
feed_event_sources(base + prime * 10_000_000)
|
|
self.assertTrue(sd.sm.all_checks())
|
|
|
|
def step(tick, *, timeout=False, old_intent=False, allowed=True, pause=False, latched=True,
|
|
brake=False, steering=False, gas=False, user_disable=False, observed_state=None, authority_unavailable=False, companion=True,
|
|
long_allowed=True, pause_long=False, state_stamp=None, intent_message=None, offset_ns=0,
|
|
native_request_lat=True, native_request_long=None, native_valid=True, panda_fault=False, panda_controls_allowed=None,
|
|
critical_event=None, calibrated=True, car_output_valid=True, relay_fault=False,
|
|
car_state_message=None, car_output_message=None, authority_message=None, panda_state_message=None, safety_message=None):
|
|
now = base + tick * 10_000_000 + offset_ns
|
|
clock.return_value = now
|
|
if seconds is not None:
|
|
seconds.return_value = now / 1e9
|
|
if car_state_message is None:
|
|
state = car_state(brake=brake) if observed_state is None else observed_state
|
|
state.cruiseState.available = True
|
|
state.steeringPressed, state.gasPressed = steering, gas
|
|
else:
|
|
# Opt-in producer-envelope path: do not reconstruct/modify raw state.
|
|
state = car_state_message.carState
|
|
if panda_controls_allowed is None:
|
|
panda_controls_allowed = full_op if native_request_long is None else native_request_long
|
|
if car_state_message is None:
|
|
state_event = messaging.new_message('carState', valid=True, logMonoTime=now if state_stamp is None else state_stamp)
|
|
state_event.carState = state
|
|
else:
|
|
state_event = car_state_message
|
|
recv.return_value = None if timeout else state_event.as_reader()
|
|
frames = []
|
|
for service in ('deviceState', 'driverMonitoringState', 'extrinsicsCalibration', 'modelV2'):
|
|
message = messaging.new_message(service, valid=True, logMonoTime=now)
|
|
if service == 'deviceState':
|
|
message.deviceState.started, message.deviceState.startedMonoTime = True, base - 1_000_000_000
|
|
if genuine_events:
|
|
message.deviceState.freeSpacePercent = 50.
|
|
elif service == 'extrinsicsCalibration':
|
|
message.extrinsicsCalibration.calStatus = 'calibrated' if calibrated else 'uncalibrated'
|
|
if genuine_events:
|
|
message.extrinsicsCalibration.rpyCalib = [0., 0., 0.]
|
|
frames.append(message.as_reader())
|
|
pandas = messaging.new_message('pandaStates', len(cp.safetyConfigs), valid=True, logMonoTime=now)
|
|
for panda, config in zip(pandas.pandaStates, cp.safetyConfigs, strict=True):
|
|
panda.safetyModel, panda.safetyParam = config.safetyModel, config.safetyParam
|
|
panda.alternativeExperience = cp.alternativeExperience
|
|
panda.controlsAllowed = panda_controls_allowed
|
|
panda.safetyRxChecksInvalid = panda_fault
|
|
if relay_fault:
|
|
panda.faults = ['relayMalfunction']
|
|
if panda_state_message is not None:
|
|
pandas = panda_state_message
|
|
frames.append(pandas.as_reader())
|
|
if car_output_message is not None:
|
|
frames.append(car_output_message.as_reader())
|
|
intent_ns = base if old_intent else now
|
|
intent = messaging.new_message('aolIntentWire', 0, valid=True, logMonoTime=intent_ns)
|
|
intent.aolIntentWire = encode_intent(IntentState('authority-card', tick + 1, intent_ns, intent_ns,
|
|
intent_ns + 200_000_000, latched, pause, pause_long, True, latched))
|
|
if intent_message is not None:
|
|
intent = intent_message
|
|
native = messaging.new_message('aolSafetyWire', 0, valid=native_valid, logMonoTime=now)
|
|
native.aolSafetyWire = encode_safety(SafetyState(1, True, now, now + 200_000_000,
|
|
int(cp.safetyConfigs[-1].safetyModel.raw), cp.safetyConfigs[-1].safetyParam,
|
|
allowed, (full_op or native_request_long is True) and long_allowed, native_request_lat,
|
|
full_op if native_request_long is None else native_request_long, 'authority-panda', sd.aol_session_id,
|
|
now if genuine_events else 0, tuple(
|
|
PandaSlot(index, 'authority-panda' if index == len(cp.safetyConfigs) - 1 else f'silent-panda-{index}',
|
|
int(config.safetyModel.raw), config.safetyParam,
|
|
cp.alternativeExperience, panda_controls_allowed, panda_fault, False,
|
|
1 if relay_fault else 0)
|
|
for index, config in enumerate(cp.safetyConfigs)) if genuine_events else ()))
|
|
if safety_message is not None:
|
|
native = safety_message
|
|
if authority_message is not None:
|
|
frames.append(authority_message.as_reader())
|
|
elif companion:
|
|
authority = messaging.new_message('starpilotCarState', valid=companion != 'invalid',
|
|
logMonoTime=now - 1 if companion == 'wrong_stamp' else now)
|
|
authority.starpilotCarState.sourceCarStateMonoTime = now - 1 if companion == 'wrong_source' else now
|
|
authority.starpilotCarState.lateralAuthorityUnavailable = authority_unavailable
|
|
frames.append(authority.as_reader())
|
|
# Mirror this producer envelope on the real nonconflating receipt,
|
|
# as Card does. The conflating SubMaster view alone proves no receipt.
|
|
if getattr(sd, 'steering_companion', None) is not None:
|
|
steering_receipts.append(authority.as_reader())
|
|
if genuine_events:
|
|
feed_event_sources(now, frames + [intent.as_reader(), native.as_reader()], car_output_valid=car_output_valid)
|
|
else:
|
|
if previous_control_message[0] is not None:
|
|
frames.append(previous_control_message[0])
|
|
sd.sm.update_msgs(now / 1e9, frames + [intent.as_reader(), native.as_reader()])
|
|
if user_disable:
|
|
self.enterContext(mock.patch.object(sd, 'update_events', lambda cs: (
|
|
sd.events.clear(), sd._update_steering_authority(cs), sd.events.add(log.OnroadEvent.EventName.pedalPressed))))
|
|
if critical_event is not None:
|
|
self.enterContext(mock.patch.object(sd, 'update_events', lambda cs: (
|
|
sd.events.clear(), sd._update_steering_authority(cs), sd.events.add(critical_event))))
|
|
sd.step()
|
|
control_frames = []
|
|
with mock.patch.object(controls.sm, 'update_msgs', side_effect=lambda _timestamp, messages: control_frames.extend(messages)):
|
|
feed(controls, now, tick * 5, active=False, enabled=False)
|
|
model = messaging.new_message('modelV2', valid=True, logMonoTime=now)
|
|
model.modelV2 = next(message.modelV2 for message in control_frames if message.which() == 'modelV2')
|
|
model.modelV2.action.desiredCurvature = 0.
|
|
if car_state_message is None:
|
|
control_state = messaging.new_message('carState', valid=sd.conditional_car_state_valid,
|
|
logMonoTime=sd.aol_car_state_log_ns)
|
|
control_state.carState = sd.CS_prev
|
|
else:
|
|
control_state = car_state_message
|
|
if car_output_message is not None:
|
|
control_frames = [sample for sample in control_frames if sample.which() != 'carOutput']
|
|
control_frames.append(car_output_message.as_reader())
|
|
if panda_state_message is not None and 'pandaStates' in controls.sm.services:
|
|
control_frames = [sample for sample in control_frames if sample.which() != 'pandaStates']
|
|
if now >= controls_panda_due[0]:
|
|
control_frames.append(panda_state_message.as_reader())
|
|
controls_panda_due[0] = now + 100_000_000
|
|
replaced = ('carState', 'aolSafetyWire', 'modelV2', 'aolAxisState', 'selfdriveState')
|
|
controls.sm.update_msgs(now / 1e9, [message for message in control_frames if message.which() not in replaced] +
|
|
[control_state.as_reader(), native.as_reader(), model.as_reader(),
|
|
sd.pm.messages['aolAxisState'], sd.pm.messages['selfdriveState']])
|
|
command, lac_log = controls.state_control()
|
|
controls.publish(command, lac_log)
|
|
previous_control_message[0] = controls.pm.messages['carControl']
|
|
if genuine_events:
|
|
previous_control[0] = command.as_reader()
|
|
return sd.pm.messages['aolAxisState'].aolAxisState, sd.pm.messages['selfdriveState'].selfdriveState, command
|
|
return sd, controls, step
|
|
|
|
def _ioniq_availability_callers(self, full_op=True, *, initially_armed=True, stock_angle=False):
|
|
from opendbc.car.hyundai.interface import CarInterface
|
|
from opendbc.car.hyundai.radar_interface import RadarInterface
|
|
from openpilot.selfdrive.car.card import Car
|
|
from openpilot.selfdrive.car.tests.test_hyundai_aol import candidate
|
|
|
|
previous_simulation = os.environ.get('SIMULATION')
|
|
|
|
def restore_simulation():
|
|
if previous_simulation is None:
|
|
os.environ.pop('SIMULATION', None)
|
|
else:
|
|
os.environ['SIMULATION'] = previous_simulation
|
|
|
|
self.addCleanup(restore_simulation)
|
|
os.environ['SIMULATION'] = '0'
|
|
if stock_angle:
|
|
from opendbc.car.hyundai.tests.test_stock_angle_aol import source_profile
|
|
from opendbc.car.hyundai.values import CAR
|
|
cp = source_profile(CAR.HYUNDAI_IONIQ_5_PE if stock_angle is True else CAR(stock_angle))
|
|
else:
|
|
_, cp = candidate(True)
|
|
cp.safetyConfigs[-1].safetyParam |= 0x0800
|
|
sd, controls, step = self._authority_callers(full_op, cp=cp, genuine_events=True)
|
|
card_cp = cp.as_reader().as_builder()
|
|
card = Car(CI=CarInterface(card_cp), RI=RadarInterface(card_cp))
|
|
self.assertEqual(card.CP.to_dict(), sd.CP.to_dict())
|
|
self.assertEqual(card.CP.safetyConfigs[-1].safetyParam, cp.safetyConfigs[-1].safetyParam if stock_angle else 0x8895)
|
|
self.assertTrue(card.aol_card_intent.explicit_latch)
|
|
card.slc_producer_session = 'authority-card'
|
|
cs = car_state()
|
|
cs.cruiseState.available = True
|
|
card.aol_card_intent.update(cs, now_ns=9_980_000_000)
|
|
if initially_armed:
|
|
cs.buttonEvents = [car.CarState.ButtonEvent(type=car.CarState.ButtonEvent.Type.lkas, pressed=True)]
|
|
card.aol_card_intent.update(cs, now_ns=9_990_000_000)
|
|
cs.buttonEvents = []
|
|
self.assertEqual(card.aol_card_intent.allowed_latch, initially_armed)
|
|
|
|
def receive(now, *, axis=None, event=None, state=None):
|
|
frames = []
|
|
for service in ('pandaStates', 'aolSafetyWire', 'deviceState', 'carControl'):
|
|
message = messaging.new_message(service, len(sd.sm[service]) if service == 'pandaStates' else
|
|
len(sd.sm[service]) if service == 'aolSafetyWire' else None,
|
|
valid=sd.sm.valid[service], logMonoTime=sd.sm.logMonoTime[service])
|
|
setattr(message, service, sd.sm[service])
|
|
# Card receives the exact envelope bytes, including validity and stamps.
|
|
frames.append(messaging.log_from_bytes(message.to_bytes()))
|
|
card.sm.update_msgs(now / 1e9, frames + [m for m in (axis, event, sd.pm.messages.get('selfdriveState')) if m is not None])
|
|
observed = state if state is not None else car_state()
|
|
if stock_angle and state is None:
|
|
observed.vEgoRaw = observed.vEgo
|
|
observed.cruiseState.available = True
|
|
card.CC_prev = card.sm['carControl']
|
|
with mock.patch('openpilot.selfdrive.car.card.messaging.drain_sock_raw', return_value=[b'fixture']), \
|
|
mock.patch('openpilot.selfdrive.car.card.can_capnp_to_list', return_value=[]), \
|
|
mock.patch('openpilot.selfdrive.car.card.time.monotonic_ns', return_value=now), \
|
|
mock.patch('openpilot.selfdrive.car.card.time.clock_gettime_ns', return_value=now), \
|
|
mock.patch('openpilot.selfdrive.car.card.REPLAY', False), \
|
|
mock.patch.object(card.CI, 'update', return_value=observed), \
|
|
mock.patch.object(card.RI, 'update', return_value=None), mock.patch.object(card.sm, 'update'):
|
|
actual, _ = card.state_update()
|
|
self.assertIs(actual, observed)
|
|
return card.aol_card_intent.allowed_latch
|
|
|
|
def drive(tick, **kwargs):
|
|
kwargs.setdefault('latched', card.aol_card_intent.allowed_latch)
|
|
if stock_angle and 'observed_state' not in kwargs:
|
|
state = car_state(brake=kwargs.get('brake', False))
|
|
state.vEgoRaw = state.vEgo
|
|
kwargs['observed_state'] = state
|
|
return step(tick, **kwargs)
|
|
|
|
for tick in (0, 1):
|
|
axis, _, command = drive(tick)
|
|
if full_op:
|
|
self.assertTrue(axis.lateralActive and axis.longitudinalActive and command.latActive and command.longActive)
|
|
else:
|
|
self.assertEqual(bool(axis.lateralActive and command.latActive), initially_armed)
|
|
self.assertFalse(sd.enabled or sd.active or axis.longitudinalActive or command.longActive)
|
|
self.assertEqual(receive(10_000_000_000 + tick * 10_000_000,
|
|
axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents']), initially_armed)
|
|
return sd, controls, drive, card, receive
|
|
|
|
def test_full_set_engagement_arms_aol_and_brake_keeps_lateral_without_lkas(self):
|
|
from opendbc.car.hyundai.values import Buttons
|
|
|
|
with OpenpilotPrefix():
|
|
sd, _, step, card, receive = self._ioniq_availability_callers(full_op=False, initially_armed=False)
|
|
self.assertFalse(card.aol_card_intent.allowed_latch)
|
|
for tick, previous, current in ((2, Buttons.NONE, Buttons.SET_DECEL),
|
|
(3, Buttons.SET_DECEL, Buttons.NONE)):
|
|
cs = car_state()
|
|
cs.buttonEvents = card.CI.CS.create_cruise_button_events(current, previous)
|
|
cs.buttonEnable = card.CI.CS.update_button_enable(cs.buttonEvents)
|
|
step(tick, observed_state=cs, native_request_long=False)
|
|
receive(10_000_000_000 + tick * 10_000_000, state=cs,
|
|
axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
|
|
self.assertIn(log.OnroadEvent.EventName.buttonEnable, sd.events.names)
|
|
self.assertTrue(sd.enabled and sd.active)
|
|
# PandaStates updates at 10 Hz. Grant LONG only once its actual fresh
|
|
# health observation agrees, retaining the production latest-Panda veto.
|
|
for tick in range(4, 13):
|
|
granted = tick >= 10
|
|
axis, _, command = step(tick, native_request_long=granted)
|
|
self.assertTrue(axis.lateralActive and command.latActive)
|
|
self.assertEqual(bool(axis.longitudinalActive and command.longActive), granted)
|
|
receive(10_000_000_000 + tick * 10_000_000,
|
|
axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents'])
|
|
self.assertTrue(card.aol_card_intent.allowed_latch)
|
|
for tick in (13, 14, 15):
|
|
axis, _, command = step(tick, brake=tick == 13, native_request_long=False)
|
|
self.assertTrue(axis.lateralActive and command.latActive)
|
|
self.assertFalse(sd.enabled or sd.active or axis.longitudinalActive or command.longActive)
|
|
self.assertTrue(receive(10_000_000_000 + tick * 10_000_000, state=car_state(brake=tick == 13),
|
|
axis=sd.pm.messages['aolAxisState'], event=sd.pm.messages['onroadEvents']))
|
|
self.assertFalse(any(e.type == car.CarState.ButtonEvent.Type.lkas for e in card.CI.CS.out.buttonEvents))
|
|
|
|
def test_reverse_keeps_actual_card_armed_without_output_and_drive_requires_ack(self):
|
|
from openpilot.starpilot.aol.wire import decode_intent
|
|
|
|
with OpenpilotPrefix():
|
|
sd, _, step, card, receive = self._ioniq_availability_callers(full_op=False)
|
|
card.pm = type(sd.pm)()
|
|
card.aol_sequence = 2
|
|
for tick in range(2, 45):
|
|
reverse = tick < 42
|
|
cs = car_state(brake=reverse)
|
|
cs.gearShifter = car.CarState.GearShifter.reverse if reverse else car.CarState.GearShifter.drive
|
|
now = 10_000_000_000 + tick * 10_000_000
|
|
original_message = messaging.new_message
|
|
def current_message(*args, _original=original_message, _now=now, **kwargs):
|
|
message = _original(*args, **kwargs)
|
|
message.logMonoTime = _now
|
|
return message
|
|
with mock.patch('openpilot.selfdrive.car.card.messaging.new_message', side_effect=current_message):
|
|
card.state_publish(cs, None)
|
|
intent_message = card.pm.messages['aolIntentWire'].as_builder()
|
|
intent = decode_intent(bytes(intent_message.aolIntentWire))
|
|
self.assertTrue(intent.lateralArmed)
|
|
self.assertEqual(intent.allowedLatch, not reverse)
|
|
requested = tick == 2 or tick >= 43
|
|
axis, state, command = step(tick, observed_state=cs, intent_message=intent_message,
|
|
native_request_lat=requested, allowed=requested, native_request_long=False)
|
|
self.assertEqual(bool(axis.lateralActive), tick >= 43)
|
|
self.assertEqual(bool(command.latActive), tick >= 43)
|
|
self.assertFalse(axis.longitudinalActive or command.longActive or sd.enabled or sd.active)
|
|
self.assertTrue(axis.nativeAcknowledged, (tick, sd.events.names,
|
|
[(str(sample.gearShifter), sample.canValid, sample.canTimeout) for sample, _ in sd.car_state_samples], axis.to_dict()))
|
|
self.assertEqual(str(axis.faultReason), 'none')
|
|
if reverse:
|
|
self.assertIn(log.OnroadEvent.EventName.reverseGear, sd.events.names)
|
|
self.assertIn(log.OnroadEvent.EventName.wrongGear, sd.events.names)
|
|
self.assertFalse(axis.desiredLateral or axis.desiredLongitudinal)
|
|
self.assertTrue(receive(now, state=cs, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents']))
|
|
|
|
def test_ioniq_caroutput_communication_fault_joined_delivery_and_recovery(self):
|
|
for order in ('axis_first', 'event_first', 'together'):
|
|
with self.subTest(order=order), OpenpilotPrefix():
|
|
sd, _, step, card, receive = self._ioniq_availability_callers()
|
|
axis, state, command = step(2, car_output_valid=False)
|
|
self.assertFalse(sd.sm.valid['carOutput'])
|
|
self.assertTrue(sd.sm.all_checks(['carControl']))
|
|
self.assertTrue(sd.CS_prev.canValid and not sd.CS_prev.canTimeout)
|
|
self.assertIn(log.OnroadEvent.EventName.commIssue, sd.events.names)
|
|
self.assertTrue(sd.events.contains(ET.SOFT_DISABLE))
|
|
self.assertFalse(axis.lateralActive or axis.longitudinalActive or command.latActive or command.longActive)
|
|
self.assertEqual(state.alertType, 'commIssue/softDisable')
|
|
self.assertEqual(state.alertText2, 'Communication Issue Between Processes')
|
|
# The shared SOFT owner retains armed intent while both output axes
|
|
# are withdrawn; source/event joins still have their original deadline.
|
|
proof, event = sd.pm.messages['aolAxisState'], sd.pm.messages['onroadEvents']
|
|
self.assertEqual(proof.aolAxisState.faultEventMonoTime, event.logMonoTime)
|
|
now = 10_020_000_000
|
|
if order == 'axis_first':
|
|
self.assertTrue(receive(now, axis=proof))
|
|
self.assertTrue(receive(now + 1_000_000, event=event))
|
|
elif order == 'event_first':
|
|
self.assertTrue(receive(now, event=event))
|
|
self.assertTrue(receive(now + 1_000_000, axis=proof))
|
|
else:
|
|
self.assertTrue(receive(now, axis=proof, event=event))
|
|
self.assertEqual(str(axis.faultReason), 'availabilityPause')
|
|
self.assertFalse(sd.aol_authority_lost)
|
|
self.assertTrue(card.aol_transport_feedback.ordinary_warning_qualified)
|
|
self.assertFalse(card.aol_transport_feedback.retention_qualified)
|
|
self.assertFalse(card.aol_transport_feedback.qualified)
|
|
self.assertEqual(proof.aolAxisState.intentProducerSessionId, card.slc_producer_session)
|
|
self.assertEqual(proof.aolAxisState.intentIntegrityEpoch, sd.aol_intent_companion.integrity_epoch)
|
|
recovered, _, command = step(3, native_request_long=False)
|
|
self.assertTrue(recovered.lateralActive and command.latActive)
|
|
self.assertFalse(recovered.desiredLongitudinal or recovered.longitudinalActive or command.longActive)
|
|
self.assertTrue(receive(10_030_000_000, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents']))
|
|
|
|
def test_ioniq_availability_reengagement_requires_real_set_release_and_fresh_long_ack(self):
|
|
from opendbc.car.hyundai.values import Buttons
|
|
|
|
with OpenpilotPrefix():
|
|
sd, _, step, card, receive = self._ioniq_availability_callers()
|
|
occurrence = 0
|
|
# Qualified availability keeps published ordinary state disabled;
|
|
# retained lateral intent does not itself reengage LONG.
|
|
for tick in range(2, 313):
|
|
axis, _, command = step(tick, car_output_valid=False)
|
|
self.assertFalse(axis.lateralActive or axis.longitudinalActive or command.latActive or command.longActive)
|
|
published = sd.pm.messages['selfdriveState'].selfdriveState
|
|
self.assertFalse(sd.enabled or sd.active or published.enabled or published.active)
|
|
event = sd.pm.messages['onroadEvents'] if axis.faultEventMonoTime != occurrence else None
|
|
occurrence = axis.faultEventMonoTime
|
|
self.assertTrue(receive(10_000_000_000 + tick * 10_000_000,
|
|
axis=sd.pm.messages['aolAxisState'], event=event))
|
|
self.assertFalse(sd.enabled or sd.active)
|
|
recovered, _, command = step(313, native_request_long=False)
|
|
self.assertTrue(recovered.lateralActive and command.latActive)
|
|
published = sd.pm.messages['selfdriveState'].selfdriveState
|
|
self.assertFalse(sd.enabled or sd.active or published.enabled or published.active)
|
|
self.assertFalse(recovered.desiredLongitudinal or recovered.longitudinalActive or command.longActive)
|
|
self.assertTrue(receive(13_130_000_000, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents']))
|
|
|
|
pressed = car_state()
|
|
pressed.cruiseState.available = True
|
|
pressed.buttonEvents = card.CI.CS.create_cruise_button_events(Buttons.SET_DECEL, Buttons.NONE)
|
|
pressed.buttonEnable = card.CI.CS.update_button_enable(pressed.buttonEvents)
|
|
self.assertFalse(pressed.buttonEnable)
|
|
axis, _, command = step(314, observed_state=pressed, native_request_long=False)
|
|
self.assertNotIn(log.OnroadEvent.EventName.buttonEnable, sd.events.names)
|
|
self.assertFalse(sd.enabled or axis.longitudinalActive or command.longActive)
|
|
self.assertTrue(receive(13_140_000_000, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents'], state=pressed))
|
|
|
|
released = car_state()
|
|
released.cruiseState.available = True
|
|
released.buttonEvents = card.CI.CS.create_cruise_button_events(Buttons.NONE, Buttons.SET_DECEL)
|
|
released.buttonEnable = card.CI.CS.update_button_enable(released.buttonEvents)
|
|
self.assertTrue(released.buttonEnable)
|
|
pending, _, command = step(315, observed_state=released, native_request_long=False)
|
|
self.assertIn(log.OnroadEvent.EventName.buttonEnable, sd.events.names)
|
|
self.assertTrue(sd.events.contains(ET.ENABLE))
|
|
self.assertFalse(sd.events.contains(ET.NO_ENTRY))
|
|
self.assertTrue(sd.enabled and sd.active and pending.desiredLongitudinal)
|
|
self.assertTrue(pending.lateralActive and command.latActive)
|
|
self.assertFalse(pending.longitudinalActive or command.longActive)
|
|
self.assertTrue(receive(13_150_000_000, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents'], state=released))
|
|
|
|
combined, _, command = step(316, native_request_long=True)
|
|
native = current_native(sd.sm, sd.CP, now_ns=13_160_000_000, axis_session_id=sd.aol_session_id)
|
|
self.assertIsNotNone(native)
|
|
self.assertTrue(native.requestedLongitudinal and native.longitudinalAllowed)
|
|
self.assertEqual(native.observedMonoTime, 13_160_000_000)
|
|
self.assertTrue(combined.lateralActive and combined.longitudinalActive and command.latActive and command.longActive)
|
|
self.assertTrue(receive(13_160_000_000, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents']))
|
|
|
|
def test_ioniq_aol_only_availability_prompt_reaches_controls_and_clears_on_recovery(self):
|
|
with OpenpilotPrefix():
|
|
sd, controls, step, card, receive = self._ioniq_availability_callers(full_op=False)
|
|
axis, state, command = step(2, car_output_valid=False)
|
|
self.assertEqual(str(axis.faultReason), 'availabilityPause')
|
|
self.assertEqual(state.alertType, 'commIssue/softDisable')
|
|
self.assertEqual((state.alertText1, state.alertText2), ('TAKE CONTROL IMMEDIATELY', 'Communication Issue Between Processes'))
|
|
self.assertEqual(str(state.alertSound), 'warningImmediate')
|
|
delivered = controls.sm['selfdriveState']
|
|
self.assertEqual(delivered.alertType, state.alertType)
|
|
self.assertEqual(str(delivered.alertSound), 'warningImmediate')
|
|
self.assertFalse(sd.enabled or sd.active or state.enabled or state.active or
|
|
axis.lateralActive or axis.longitudinalActive or command.latActive or command.longActive)
|
|
self.assertTrue(receive(10_020_000_000, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents']))
|
|
for tick in range(3, 45):
|
|
axis, state, command = step(tick, car_output_valid=False)
|
|
self.assertEqual(state.alertType, 'commIssue/softDisable')
|
|
self.assertFalse(sd.enabled or sd.active or state.enabled or state.active)
|
|
self.assertFalse(axis.lateralActive or axis.longitudinalActive or command.latActive or command.longActive)
|
|
self.assertTrue(receive(10_000_000_000 + tick * 10_000_000, axis=sd.pm.messages['aolAxisState']))
|
|
# The normal takeover alert has its original four-second lifetime;
|
|
# it outranks the optional 0.4-second availability prompt.
|
|
self.assertEqual(str(state.alertSound), 'warningImmediate')
|
|
self.assertEqual(str(controls.sm['selfdriveState'].alertSound), 'warningImmediate')
|
|
for tick in range(45, 408):
|
|
recovered, state, command = step(tick, native_request_long=False)
|
|
self.assertTrue(recovered.lateralActive and command.latActive)
|
|
self.assertFalse(sd.enabled or sd.active or state.enabled or state.active or recovered.desiredLongitudinal or
|
|
recovered.longitudinalActive or command.longActive)
|
|
self.assertTrue(receive(10_000_000_000 + tick * 10_000_000, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents']))
|
|
self.assertNotEqual(state.alertType, 'commIssue/softDisable')
|
|
self.assertNotEqual(controls.sm['selfdriveState'].alertType, 'commIssue/softDisable')
|
|
self.assertEqual(str(state.alertSound), 'none')
|
|
self.assertEqual(str(controls.sm['selfdriveState'].alertSound), 'none')
|
|
|
|
def test_ioniq_availability_sustained_advancing_proof_keeps_warning_and_no_long(self):
|
|
with OpenpilotPrefix():
|
|
sd, _, step, card, receive = self._ioniq_availability_callers()
|
|
occurrences = set()
|
|
for tick in range(2, 413):
|
|
axis, state, command = step(tick, car_output_valid=False)
|
|
now = 10_000_000_000 + tick * 10_000_000
|
|
self.assertEqual(str(axis.faultReason), 'availabilityPause')
|
|
self.assertTrue(axis.desiredLateral and axis.nativeAcknowledged)
|
|
self.assertFalse(axis.lateralActive or axis.desiredLongitudinal or axis.longitudinalActive or
|
|
command.latActive or command.longActive)
|
|
self.assertIn(log.OnroadEvent.EventName.commIssue, sd.events.names)
|
|
self.assertTrue(state.alertText1 or state.alertText2)
|
|
proof, event = sd.pm.messages['aolAxisState'], sd.pm.messages['onroadEvents']
|
|
advanced = axis.faultEventMonoTime not in occurrences
|
|
occurrences.add(axis.faultEventMonoTime)
|
|
self.assertTrue(receive(now, axis=proof, event=event if advanced else None))
|
|
self.assertTrue(card.aol_transport_feedback.ordinary_warning_qualified)
|
|
self.assertFalse(card.aol_transport_feedback.retention_qualified)
|
|
# Real periodic SD publications advance the matched occurrence beyond the
|
|
# initial 30 ms pairing window; they do not extend an unmatched occurrence.
|
|
self.assertGreaterEqual(len(occurrences), 4)
|
|
self.assertFalse(sd.enabled or sd.active)
|
|
self.assertEqual(state.alertType, 'aolAvailability/warning')
|
|
self.assertEqual((state.alertText1, state.alertText2), ('Steering Paused', 'Communication Issue'))
|
|
recovered, _, command = step(413, native_request_long=False)
|
|
self.assertTrue(recovered.lateralActive and command.latActive)
|
|
self.assertFalse(recovered.desiredLongitudinal or recovered.longitudinalActive or command.longActive)
|
|
self.assertFalse(sd.enabled or sd.active)
|
|
self.assertTrue(receive(14_130_000_000, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents']))
|
|
|
|
def test_ioniq_availability_missing_pair_has_fixed_deadline(self):
|
|
for missing in ('axis', 'event'):
|
|
with self.subTest(missing=missing), OpenpilotPrefix():
|
|
sd, _, step, card, receive = self._ioniq_availability_callers()
|
|
step(2, car_output_valid=False)
|
|
proof, event = sd.pm.messages['aolAxisState'], sd.pm.messages['onroadEvents']
|
|
now = 10_020_000_000
|
|
self.assertTrue(receive(now, axis=proof if missing == 'event' else None,
|
|
event=event if missing == 'axis' else None))
|
|
step(4, car_output_valid=False, native_request_long=False)
|
|
renewed = sd.pm.messages['aolAxisState']
|
|
self.assertTrue(receive(now + 20_000_000, axis=renewed if missing == 'event' else None,
|
|
event=event if missing == 'axis' else None))
|
|
self.assertFalse(receive(now + 31_000_000, axis=renewed if missing == 'event' else None))
|
|
self.assertFalse(receive(now + 32_000_000, axis=proof, event=event))
|
|
self.assertFalse(card.aol_card_intent.allowed_latch)
|
|
|
|
def test_ioniq_availability_never_masks_native_can_or_integrity_fault(self):
|
|
for fault in ('native_missing', 'native_denied', 'permanent_eps', 'can_invalid', 'relay',
|
|
'intent_invalid', 'producer_restart'):
|
|
with self.subTest(fault=fault), OpenpilotPrefix():
|
|
sd, _, step, card, receive = self._ioniq_availability_callers()
|
|
step(2, car_output_valid=False)
|
|
self.assertTrue(receive(10_020_000_000, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents']))
|
|
kwargs = {'car_output_valid': False, 'native_request_long': False}
|
|
observed = car_state()
|
|
if fault == 'native_missing':
|
|
kwargs['native_valid'] = False
|
|
elif fault == 'native_denied':
|
|
kwargs['allowed'] = False
|
|
elif fault == 'permanent_eps':
|
|
observed.steerFaultPermanent = True
|
|
elif fault == 'can_invalid':
|
|
observed.canValid = False
|
|
elif fault == 'relay':
|
|
kwargs['relay_fault'] = True
|
|
else:
|
|
stamp = 10_030_000_000
|
|
intent = messaging.new_message('aolIntentWire', 0, valid=fault != 'intent_invalid', logMonoTime=stamp)
|
|
intent.aolIntentWire = encode_intent(IntentState(
|
|
'restarted-card' if fault == 'producer_restart' else card.slc_producer_session,
|
|
1, stamp, stamp, stamp + 200_000_000, True, False, False, True, True))
|
|
kwargs['intent_message'] = intent
|
|
epoch_before = sd.aol_intent_companion.integrity_epoch
|
|
axis, _, command = step(3, observed_state=observed, **kwargs)
|
|
self.assertFalse(axis.lateralActive or axis.longitudinalActive or command.latActive or command.longActive)
|
|
if fault == 'native_denied':
|
|
native = current_native(sd.sm, sd.CP, now_ns=10_030_000_000, axis_session_id=sd.aol_session_id)
|
|
self.assertIsNotNone(native)
|
|
self.assertTrue(native.requestedLateral)
|
|
self.assertFalse(native.lateralAllowed)
|
|
self.assertEqual(str(axis.faultReason), 'availabilityPause')
|
|
self.assertFalse(receive(10_030_000_000, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents'], state=observed))
|
|
self.assertFalse(card.aol_card_intent.allowed_latch)
|
|
else:
|
|
if fault in ('intent_invalid', 'producer_restart'):
|
|
self.assertGreater(sd.aol_intent_companion.integrity_epoch, epoch_before)
|
|
self.assertEqual(str(axis.faultReason), 'availabilityPause')
|
|
else:
|
|
self.assertEqual(str(axis.faultReason), 'critical')
|
|
self.assertFalse(receive(10_030_000_000, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents'], state=observed))
|
|
self.assertFalse(card.aol_card_intent.allowed_latch)
|
|
|
|
def test_neutral_reverse_neutral_drive_preserves_actual_card_latch(self):
|
|
from openpilot.starpilot.aol.wire import decode_intent
|
|
|
|
with OpenpilotPrefix():
|
|
sd, _, step, card, receive = self._ioniq_availability_callers()
|
|
card.pm = type(sd.pm)()
|
|
card.aol_sequence = 2
|
|
gears = ['neutral'] * 3 + ['reverse'] * 40 + ['neutral'] * 3 + ['drive'] * 4
|
|
for tick, gear in enumerate(gears, 2):
|
|
cs = car_state(brake=True)
|
|
cs.gearShifter = gear
|
|
now = 10_000_000_000 + tick * 10_000_000
|
|
original_message = messaging.new_message
|
|
def current_message(*args, _original=original_message, _now=now, **kwargs):
|
|
message = _original(*args, **kwargs)
|
|
message.logMonoTime = _now
|
|
return message
|
|
with mock.patch('openpilot.selfdrive.car.card.messaging.new_message', side_effect=current_message):
|
|
card.state_publish(cs, None)
|
|
intent_message = card.pm.messages['aolIntentWire'].as_builder()
|
|
intent = decode_intent(bytes(intent_message.aolIntentWire))
|
|
self.assertTrue(intent.lateralArmed)
|
|
requested = gear == 'drive'
|
|
axis, state, command = step(tick, observed_state=cs, intent_message=intent_message,
|
|
native_request_lat=requested, allowed=requested, native_request_long=False)
|
|
if gear != 'drive':
|
|
self.assertFalse(axis.desiredLateral or axis.lateralActive or command.latActive)
|
|
self.assertFalse(axis.longitudinalActive or command.longActive)
|
|
self.assertEqual(str(axis.faultReason), 'none')
|
|
self.assertTrue(receive(now, state=cs, axis=sd.pm.messages['aolAxisState'],
|
|
event=sd.pm.messages['onroadEvents']))
|
|
self.assertTrue(axis.lateralActive and command.latActive)
|
|
self.assertFalse(state.enabled or state.active)
|