mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
220 lines
12 KiB
Python
220 lines
12 KiB
Python
"""Volt route c0508: source cutoff must not replace an unexpired axis proof."""
|
|
from collections import deque
|
|
from contextlib import ExitStack
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
|
|
from openpilot.cereal import messaging
|
|
from openpilot.common.prefix import OpenpilotPrefix
|
|
from openpilot.starpilot.aol.intent import read_settings
|
|
from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture
|
|
from openpilot.starpilot.aol.tests.test_input_deadline_continuity import BASE, intent
|
|
|
|
|
|
def bind_steering_receipts(sd, step, cleanup):
|
|
"""Deliver the same declared producer receipt through its actual socket path."""
|
|
if sd.steering_companion is None:
|
|
return step
|
|
pending = deque()
|
|
original = messaging.recv_one_or_none
|
|
|
|
def receive(sock):
|
|
if sock is sd.steering_companion.sock:
|
|
return pending.popleft() if pending else None
|
|
return original(sock)
|
|
|
|
cleanup.enter_context(patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one_or_none', side_effect=receive))
|
|
|
|
def observed_step(tick, **kwargs):
|
|
if not kwargs.get('timeout', False):
|
|
stamp = kwargs.get('state_stamp', 10_000_000_000 + tick * 10_000_000 + kwargs.get('offset_ns', 0))
|
|
receipt = messaging.new_message('starpilotCarState', valid=True, logMonoTime=stamp)
|
|
receipt.starpilotCarState.sourceCarStateMonoTime = stamp
|
|
receipt.starpilotCarState.lateralAuthorityUnavailable = kwargs.get('authority_unavailable', False)
|
|
kwargs['authority_message'] = receipt
|
|
pending.append(receipt.as_reader())
|
|
return step(tick, **kwargs)
|
|
|
|
return observed_step
|
|
|
|
|
|
def profiles():
|
|
from opendbc.car.gm.tests.test_volt_camera_control import camera_params
|
|
from opendbc.car.gm.tests.test_volt_camera_removed import removed_params
|
|
from opendbc.car.gm.tests.test_bolt_volt_configurations import ordinary_params
|
|
from opendbc.car.gm.tests.test_bolt_pedal import params as bolt_params
|
|
from opendbc.car.gm.values import CAR
|
|
result = [sdgm_params(alpha=True, brake_c9=c9) for c9 in (False, True)]
|
|
result += [camera_params(alpha=True)]
|
|
result += [removed_params(alpha=True, alternate=alternate) for alternate in (False, True)]
|
|
result += [ordinary_params(CAR.CHEVROLET_VOLT_ASCM, alpha=True, sascm=True,
|
|
accelerator=not c9, radar=radar) for c9 in (False, True) for radar in (False, True)]
|
|
result += [bolt_params(candidate, pedal=True, camera=not removed, removed=removed) for candidate 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) for removed in (False, True)]
|
|
return result
|
|
|
|
|
|
@pytest.mark.parametrize('cp', [cp for cp in profiles() if cp.carFingerprint != 'CHEVROLET_VOLT_ASCM'],
|
|
ids=lambda cp: f'{cp.carFingerprint}-{cp.safetyConfigs[0].safetyParam:x}')
|
|
@pytest.mark.parametrize('mode', ['ordinary', 'brake', 'regen'])
|
|
@pytest.mark.parametrize('successor', ['fresh', 'missing'])
|
|
def test_volt_cutoff_keeps_only_original_lease_then_consumes_next_source(mode, successor, cp):
|
|
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
|
with OpenpilotPrefix(), ExitStack() as cleanup:
|
|
cleanup.callback(case.doCleanups)
|
|
full = mode == 'ordinary'
|
|
sd, controls, step = case._authority_callers(full, cp=cp.as_reader().as_builder(), genuine_events=True)
|
|
step = bind_steering_receipts(sd, step, cleanup)
|
|
sd.aol_settings = read_settings(sd.params)
|
|
cs = fixture.car_state(brake=mode == 'brake')
|
|
cs.regenBraking = mode == 'regen'
|
|
old, _, command = step(2, state_stamp=BASE, offset_ns=5_467_396,
|
|
intent_message=intent(BASE, 3), observed_state=cs, native_request_long=full)
|
|
assert old.lateralActive and command.latActive and bool(command.longActive) == full
|
|
deadline = old.validUntilMonoTime
|
|
clock = [BASE + 29_900_000]
|
|
actual_controls_receive = controls.sm.update_msgs
|
|
|
|
def empty_control_receive(timeout):
|
|
assert timeout == 0
|
|
actual_controls_receive(clock[0] / 1e9, [])
|
|
|
|
cleanup.enter_context(patch.object(controls.sm, 'update', side_effect=empty_control_receive))
|
|
original = sd.update_process_health
|
|
|
|
def finish_preamble():
|
|
value = original()
|
|
clock[0] = BASE + 32_985_260
|
|
return value
|
|
|
|
with patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', side_effect=lambda: clock[0]), \
|
|
patch.object(sd, 'update_process_health', side_effect=finish_preamble):
|
|
held, _, _ = step(5, timeout=True, state_stamp=BASE, offset_ns=-100_000,
|
|
intent_message=intent(BASE, 3), observed_state=cs, native_request_long=full)
|
|
from opendbc.car.gm.steering_authority import monitored_profile
|
|
if monitored_profile(cp):
|
|
# This monitored profile also needs its original steering-health receipt.
|
|
# At this point that source is older than30ms. Its missing successor cannot
|
|
# inherit health from an unexpired axis envelope or a fresh Panda grant.
|
|
assert not sd.aol_axis_publication_deferred
|
|
assert not held.lateralActive and not held.longitudinalActive
|
|
assert sd.steering_authority_unavailable and sd.aol_authority_lost
|
|
return
|
|
assert sd.aol_axis_publication_deferred
|
|
assert held.to_dict() == old.to_dict() and held.validUntilMonoTime == deadline
|
|
assert not sd.aol_authority_lost
|
|
# Consumers continue to enforce the OLD deadline independently.
|
|
with patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=deadline + 1):
|
|
expired, _ = controls.state_control()
|
|
assert not expired.latActive and not expired.longActive
|
|
fresh = BASE + 36_000_000
|
|
axis, state, command = step(6, state_stamp=fresh, timeout=successor == 'missing',
|
|
intent_message=intent(fresh, 4) if successor == 'fresh' else intent(BASE, 3),
|
|
observed_state=cs, native_request_long=full)
|
|
assert not sd.aol_axis_publication_deferred
|
|
if successor == 'fresh':
|
|
assert axis.sourceCarStateMonoTime == fresh and axis.lateralActive and command.latActive
|
|
assert bool(command.longActive) == full and bool(state.enabled) == full
|
|
assert not sd.aol_authority_lost
|
|
else:
|
|
assert not axis.lateralActive and not axis.longitudinalActive
|
|
assert not command.latActive and not command.longActive
|
|
|
|
|
|
@pytest.mark.parametrize('bad', ['cancel', 'new_brake', 'new_paddle', 'door', 'belt', 'temporary_eps',
|
|
'permanent_eps', 'invalid_can', 'main_off', 'native_deny', 'native_rx', 'producer', 'pause', 'deadline'])
|
|
def test_volt_deferral_cannot_hide_driver_edges_or_failed_proof(bad):
|
|
from dataclasses import replace
|
|
from opendbc.car.structs import car
|
|
from openpilot.cereal import messaging
|
|
from openpilot.starpilot.aol.wire import decode_intent, encode_intent, decode_safety, encode_safety
|
|
|
|
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
|
with OpenpilotPrefix(), ExitStack() as cleanup:
|
|
cleanup.callback(case.doCleanups)
|
|
sd, _, step = case._authority_callers(False, cp=sdgm_params(alpha=True), genuine_events=True)
|
|
step = bind_steering_receipts(sd, step, cleanup)
|
|
sd.aol_settings = read_settings(sd.params)
|
|
old, _, command = step(2, state_stamp=BASE, offset_ns=4_530_833,
|
|
intent_message=intent(BASE, 3), native_request_long=False)
|
|
assert old.lateralActive and command.latActive
|
|
now = BASE + 30_721_946
|
|
cs = sd.CS_prev.as_builder()
|
|
if bad == 'cancel':
|
|
cs.buttonEvents = [car.CarState.ButtonEvent(type='cancel', pressed=True)]
|
|
elif bad in ('new_brake', 'new_paddle', 'door', 'belt', 'temporary_eps', 'permanent_eps', 'invalid_can', 'main_off'):
|
|
if bad == 'main_off':
|
|
cs.cruiseState.available = False
|
|
else:
|
|
setattr(cs, {'new_brake': 'brakePressed', 'new_paddle': 'regenBraking', 'door': 'doorOpen',
|
|
'belt': 'seatbeltUnlatched', 'temporary_eps': 'steerFaultTemporary', 'permanent_eps': 'steerFaultPermanent',
|
|
'invalid_can': 'canValid'}[bad], bad != 'invalid_can')
|
|
elif bad.startswith('native_'):
|
|
native = decode_safety(sd.sm['aolSafetyWire'])
|
|
native = replace(native, lateralAllowed=False) if bad == 'native_deny' else replace(native,
|
|
pandaInventory=tuple(replace(p, safetyRxChecksInvalid=True) for p in native.pandaInventory))
|
|
message = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=native.observedMonoTime)
|
|
message.aolSafetyWire = encode_safety(native)
|
|
sd.sm.update_msgs(now / 1e9, [message.as_reader()])
|
|
elif bad in ('producer', 'pause'):
|
|
message = intent(BASE + 29_000_000, 4)
|
|
value = decode_intent(message.aolIntentWire)
|
|
message.aolIntentWire = encode_intent(replace(value, **(
|
|
{'producerSessionId': 'different-card'} if bad == 'producer' else {'pauseLateral': True})))
|
|
sd.aol_intent_companion._observe(message.logMonoTime, message.valid, message.aolIntentWire)
|
|
else:
|
|
now = old.validUntilMonoTime
|
|
initial = (sd.state_machine.state, sd.state_machine.soft_disable_timer, sd.enabled, sd.active)
|
|
sd.car_state_samples = ((cs.as_reader(), True),)
|
|
with patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=now):
|
|
assert not sd.defer_expired_axis_publication(cs, initial, now_ns=now)
|
|
assert old.validUntilMonoTime == BASE + 34_530_833
|
|
|
|
|
|
@pytest.mark.parametrize('cp', [cp for cp in profiles() if cp.carFingerprint == 'CHEVROLET_VOLT_ASCM'],
|
|
ids=lambda cp: f'{cp.safetyConfigs[0].safetyParam:x}')
|
|
def test_ascm_separate_steering_companion_expiry_still_withdraws_outputs(cp):
|
|
from openpilot.cereal import messaging
|
|
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
|
with OpenpilotPrefix(), ExitStack() as cleanup:
|
|
cleanup.callback(case.doCleanups)
|
|
sd, _, step = case._authority_callers(True, cp=cp.as_reader().as_builder(), genuine_events=True)
|
|
step = bind_steering_receipts(sd, step, cleanup)
|
|
sd.aol_settings = read_settings(sd.params)
|
|
from collections import deque
|
|
queue = deque()
|
|
original_update = sd.sm.update_msgs
|
|
original_receive = messaging.recv_one_or_none
|
|
def update(timestamp, frames):
|
|
queue.extend(frame for frame in frames if frame.which() == 'starpilotCarState')
|
|
return original_update(timestamp, frames)
|
|
def receive(sock):
|
|
if sock is sd.steering_companion.sock:
|
|
return queue.popleft() if queue else None
|
|
return original_receive(sock)
|
|
cleanup.enter_context(patch.object(sd.sm, 'update_msgs', side_effect=update))
|
|
cleanup.enter_context(patch.object(messaging, 'recv_one_or_none', side_effect=receive))
|
|
authority = messaging.new_message('starpilotCarState', valid=True, logMonoTime=BASE)
|
|
authority.starpilotCarState.sourceCarStateMonoTime = BASE
|
|
# This family has an ADDITIONAL source-bound steering companion. Its
|
|
# original lifetime is independent of the axis publication's later expiry.
|
|
old, _, command = step(2, state_stamp=BASE, offset_ns=5_467_396,
|
|
intent_message=intent(BASE, 3), authority_message=authority)
|
|
assert old.lateralActive and command.latActive and command.longActive
|
|
clock = [BASE + 29_900_000]
|
|
original = sd.update_process_health
|
|
def finish_preamble():
|
|
value = original()
|
|
clock[0] = BASE + 32_985_260
|
|
return value
|
|
with patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', side_effect=lambda: clock[0]), \
|
|
patch.object(sd, 'update_process_health', side_effect=finish_preamble):
|
|
axis, _, command = step(5, timeout=True, offset_ns=-100_000,
|
|
intent_message=intent(BASE, 3), authority_message=authority)
|
|
assert not sd.aol_axis_publication_deferred
|
|
assert not axis.lateralActive and not command.latActive and not command.longActive
|
|
assert old.validUntilMonoTime == BASE + 35_467_396
|