mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
215 lines
11 KiB
Python
215 lines
11 KiB
Python
"""Shared source-pause intent contract for every registered make and Volt owner.
|
|
|
|
Native peer wires are controlled fixtures; no physical USB qualification is claimed.
|
|
"""
|
|
from dataclasses import replace
|
|
from functools import lru_cache
|
|
|
|
import pytest
|
|
|
|
from opendbc.car import structs
|
|
from openpilot.cereal import messaging
|
|
from openpilot.common.prefix import OpenpilotPrefix
|
|
from openpilot.starpilot.aol import vehicle
|
|
from openpilot.starpilot.aol.intent import AolSettings
|
|
from openpilot.starpilot.aol.runtime import decide_axes
|
|
from openpilot.starpilot.aol.tests.test_crossmake_native_admission import BRANDS, profile
|
|
from openpilot.starpilot.aol.tests.test_gm_held_native import Peers
|
|
from openpilot.starpilot.aol.tests.test_panda_inventory import bound_state
|
|
from openpilot.starpilot.aol.transport_pause import TransportPauseFeedback
|
|
from openpilot.starpilot.aol.wire import encode_safety
|
|
|
|
NOW = 10_000_000_000
|
|
PRODUCER, SESSION = 'global-source-card', 'global-source-axis'
|
|
VOLTS = ('volt_cc', 'volt_gateway', 'volt_ascm_stock', 'volt_ascm_alpha', 'volt_sdgm_stock', 'volt_sdgm_alpha',
|
|
'volt_camera_stock', 'volt_camera_alpha')
|
|
PROFILES = (*BRANDS, *VOLTS)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def namespace():
|
|
with OpenpilotPrefix():
|
|
yield
|
|
|
|
|
|
@lru_cache(maxsize=len(PROFILES))
|
|
def configured_bytes(identity):
|
|
if identity in BRANDS:
|
|
cp = profile(identity)
|
|
else:
|
|
from opendbc.car.gm.values import CAR
|
|
if identity == 'volt_cc':
|
|
from opendbc.car.gm.tests.test_volt_cc_f1 import params
|
|
cp = params()
|
|
elif identity in ('volt_gateway', 'volt_ascm_stock', 'volt_ascm_alpha'):
|
|
from opendbc.car.gm.tests.test_bolt_volt_configurations import ordinary_params
|
|
cp = ordinary_params(CAR.CHEVROLET_VOLT if identity == 'volt_gateway' else CAR.CHEVROLET_VOLT_ASCM,
|
|
alpha=identity.endswith('alpha'), sascm=identity != 'volt_gateway', radar=True)
|
|
elif identity.startswith('volt_sdgm'):
|
|
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
|
|
cp = sdgm_params(alpha=identity.endswith('alpha'), radar=True)
|
|
else:
|
|
from opendbc.car.gm.tests.test_volt_camera_control import camera_params
|
|
cp = camera_params(alpha=identity.endswith('alpha'), radar=True)
|
|
policy = vehicle.policy_for(cp)
|
|
assert policy.intent_supported and not cp.dashcamOnly
|
|
cp.safetyConfigs[-1].safetyParam |= policy.safety_param_addition
|
|
cp.alternativeExperience |= policy.alternative_experience_addition
|
|
assert vehicle.native_matches_cp(cp, cp.safetyConfigs[-1].safetyModel.raw, cp.safetyConfigs[-1].safetyParam)
|
|
return cp.as_reader().as_builder().to_bytes()
|
|
|
|
|
|
def parameters(identity):
|
|
with structs.CarParams.from_bytes(configured_bytes(identity)) as cp:
|
|
return cp.as_builder()
|
|
|
|
|
|
def state():
|
|
cs = structs.CarState(canValid=True, gearShifter='drive', vEgo=20., vEgoRaw=20.)
|
|
cs.cruiseState.available = True
|
|
return cs
|
|
|
|
|
|
def sample(cp, now, sequence, *, phase='active', withdrawn=False, order='matched', warning='commIssue'):
|
|
longitudinal = bool(cp.openpilotLongitudinalControl)
|
|
native = bound_state(cp, now=now, session=SESSION)
|
|
native = replace(native, requestedLongitudinal=longitudinal, longitudinalAllowed=longitudinal)
|
|
if withdrawn:
|
|
native = replace(native, requestedLateral=False, lateralAllowed=False, requestedLongitudinal=False, longitudinalAllowed=False)
|
|
wire = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=now)
|
|
wire.aolSafetyWire = encode_safety(native)
|
|
pandas = messaging.new_message('pandaStates', len(cp.safetyConfigs), valid=True, logMonoTime=native.sourcePandaStatesMonoTime)
|
|
for panda, slot in zip(pandas.pandaStates, native.pandaInventory, strict=True):
|
|
panda.safetyModel, panda.safetyParam = slot.safetyModel, slot.safetyParam
|
|
panda.alternativeExperience, panda.controlsAllowed = slot.alternativeExperience, slot.controlsAllowed
|
|
proof = messaging.new_message('aolAxisState', valid=True, logMonoTime=now)
|
|
axis = proof.aolAxisState
|
|
axis.sessionId = axis.faultSessionId = SESSION
|
|
axis.intentProducerSessionId, axis.intentIntegrityEpoch = PRODUCER, 0
|
|
axis.sequence, axis.qualified = sequence, True
|
|
axis.observedMonoTime, axis.validUntilMonoTime = now, now + 30_000_000
|
|
axis.sourceCarStateMonoTime = 0 if phase == 'pause' else now
|
|
axis.desiredLateral = phase != 'pause'
|
|
axis.desiredLongitudinal = longitudinal and phase != 'pause'
|
|
axis.lateralActive = phase == 'active'
|
|
axis.longitudinalActive = longitudinal and phase == 'active'
|
|
axis.nativeAcknowledged = phase != 'pause'
|
|
axis.mode = ('combined' if longitudinal else 'lateralOnly') if phase == 'active' else 'off'
|
|
axis.faultReason = 'availabilityPause' if phase == 'pause' else 'none'
|
|
axis.faultEventMonoTime = now if phase == 'pause' else NOW
|
|
matched = phase == 'pause' and order == 'matched'
|
|
event_stamp = now if matched else now + 1_000_000 if order == 'recovery_first' else NOW
|
|
events = messaging.new_message('onroadEvents', int(matched), valid=True, logMonoTime=event_stamp)
|
|
if matched:
|
|
events.onroadEvents[0].name, events.onroadEvents[0].softDisable = warning, True
|
|
return Peers([wire.as_reader(), pandas.as_reader(), proof.as_reader(), events.as_reader()]), native
|
|
|
|
|
|
def baseline(identity):
|
|
cp, cs = parameters(identity), state()
|
|
feedback = TransportPauseFeedback(cp)
|
|
for tick, phase in ((0, 'active'), (10_000_000, 'inactive')):
|
|
sm, _ = sample(cp, NOW + tick, tick + 1, phase=phase)
|
|
assert not feedback.observe(sm, cp, cs, NOW + tick, latched=True, intent_producer_session=PRODUCER)
|
|
assert feedback.ever_lateral_active and not feedback.lateral_only_baseline
|
|
return cp, cs, feedback
|
|
|
|
|
|
def change(sm, service, update):
|
|
event = sm.messages[service].as_builder()
|
|
update(getattr(event, service))
|
|
sm.messages[service] = event.as_reader()
|
|
|
|
|
|
@pytest.mark.parametrize('identity', PROFILES)
|
|
@pytest.mark.parametrize('withdrawn', [False, True])
|
|
@pytest.mark.parametrize('order', ['matched', 'axis_first', 'recovery_first'])
|
|
@pytest.mark.parametrize('warning', ['commIssue', 'commIssueAvgFreq', 'selfdrivedLagging'])
|
|
def test_every_make_retains_source_pause_intent_and_requires_current_output_grant(identity, withdrawn, order, warning):
|
|
cp, cs, feedback = baseline(identity)
|
|
now = NOW + 42_000_000
|
|
sm, _ = sample(cp, now, 20_000_001, phase='pause', withdrawn=withdrawn, order=order, warning=warning)
|
|
before = dict(vars(feedback))
|
|
assert not feedback.observe(sm, cp, cs, now + 2_000_000, latched=True, intent_producer_session=PRODUCER)
|
|
assert not feedback.failed
|
|
for key in ('signature', 'sequence', 'healthy_feedback_ns', 'healthy_native_ns', 'lateral_source_ns'):
|
|
assert vars(feedback)[key] == before[key]
|
|
recovered, native = sample(cp, now + 10_000_000, 20_000_002, phase='inactive', withdrawn=True)
|
|
assert not feedback.observe(recovered, cp, cs, now + 10_000_000, latched=True, intent_producer_session=PRODUCER)
|
|
intent = type('Intent', (), {'allowedLatch': True, 'pauseLateral': False, 'pauseLongitudinal': False})()
|
|
def decision(native):
|
|
return decide_axes(standard_lateral=False, standard_longitudinal=False, intent=intent, native=native, car_state=cs,
|
|
initialized=True, model_ready=True, no_entry=False, immediate_disable=False,
|
|
dm_lockout=False, pause_brake_mps=0.)
|
|
axes = decision(native)
|
|
assert not axes.lateral_active and not axes.longitudinal_active
|
|
granted, native = sample(cp, now + 20_000_000, 20_000_003, phase='active')
|
|
assert not feedback.observe(granted, cp, cs, now + 20_000_000, latched=True, intent_producer_session=PRODUCER)
|
|
assert decision(native).lateral_active
|
|
|
|
|
|
@pytest.mark.parametrize('identity', PROFILES)
|
|
@pytest.mark.parametrize('fault', ['source_expired', 'packet_expired', 'native_expired', 'native_denial', 'native_fault',
|
|
'can', 'timeout', 'permanent_eps', 'button', 'main_off', 'producer', 'epoch',
|
|
'session', 'cp', 'event_fault', 'event_cancel', 'combined_cancel', 'unarmed'])
|
|
def test_every_make_keeps_existing_fault_cancel_identity_and_stale_vetoes(identity, fault):
|
|
cp, cs, feedback = baseline(identity)
|
|
now = NOW + (210_000_001 if fault == 'source_expired' else 42_000_000)
|
|
sm, native = sample(cp, now, 20_000_001, phase='pause')
|
|
producer, latched = PRODUCER, fault != 'unarmed'
|
|
if fault == 'packet_expired':
|
|
now += 30_000_001
|
|
elif fault in ('can', 'timeout', 'permanent_eps'):
|
|
setattr(cs, {'can': 'canValid', 'timeout': 'canTimeout', 'permanent_eps': 'steerFaultPermanent'}[fault], fault != 'can')
|
|
elif fault == 'main_off':
|
|
cs.cruiseState.available = False
|
|
elif fault == 'button':
|
|
cs.buttonEvents = [{'type': 'cancel', 'pressed': True}]
|
|
elif fault == 'producer':
|
|
producer = 'different'
|
|
elif fault in ('epoch', 'session'):
|
|
change(sm, 'aolAxisState', lambda axis: setattr(axis, 'intentIntegrityEpoch' if fault == 'epoch' else 'sessionId', 1 if fault == 'epoch' else 'different'))
|
|
elif fault == 'cp':
|
|
cp.flags ^= 1
|
|
elif fault == 'combined_cancel':
|
|
event = messaging.new_message('onroadEvents', 2, valid=True, logMonoTime=now)
|
|
event.onroadEvents[0] = sm['onroadEvents'][0]
|
|
event.onroadEvents[1].name, event.onroadEvents[1].userDisable = 'buttonCancel', True
|
|
sm.messages['onroadEvents'] = event.as_reader()
|
|
elif fault in ('event_fault', 'event_cancel'):
|
|
def alter(events):
|
|
events[0].name = 'controlsMismatch' if fault == 'event_fault' else 'buttonCancel'
|
|
events[0].immediateDisable, events[0].userDisable = fault == 'event_fault', fault == 'event_cancel'
|
|
change(sm, 'onroadEvents', alter)
|
|
elif fault in ('native_expired', 'native_denial', 'native_fault'):
|
|
updates = {'native_expired': {'validUntilMonoTime': now - 1}, 'native_denial': {'lateralAllowed': False},
|
|
'native_fault': {'pandaInventory': (*native.pandaInventory[:-1], replace(native.pandaInventory[-1], heartbeatLost=True))}}[fault]
|
|
event = sm.messages['aolSafetyWire'].as_builder()
|
|
event.aolSafetyWire = encode_safety(replace(native, **updates))
|
|
sm.messages['aolSafetyWire'] = event.as_reader()
|
|
assert not feedback._inactive_source_pause_current(sm, cp, cs, sm['aolAxisState'], sm.logMonoTime['aolAxisState'], now,
|
|
latched=latched, producer=producer)
|
|
|
|
|
|
@pytest.mark.parametrize('identity', ('gm', *VOLTS))
|
|
@pytest.mark.parametrize('withdrawn', [False, True])
|
|
@pytest.mark.parametrize('warning', ['commIssue', 'commIssueAvgFreq', 'selfdrivedLagging'])
|
|
def test_gm_and_volt_actual_intent_owner_keeps_main_arm_during_temporary_pause(identity, withdrawn, warning):
|
|
cp, cs, feedback = baseline(identity)
|
|
settings = AolSettings(True, 0., 0, 0, (0, 0, 0), (0, 0, 0))
|
|
owner = vehicle.create_intent(cp, settings, vehicle.policy_for(cp))
|
|
owner.update(cs, fault_active=False, now_ns=NOW)
|
|
assert owner.allowed_latch and not owner._main_cycle_required
|
|
now = NOW + 42_000_000
|
|
sm, _ = sample(cp, now, 20_000_001, phase='pause', withdrawn=withdrawn, warning=warning)
|
|
reset = feedback.observe(sm, cp, cs, now, latched=owner.allowed_latch, intent_producer_session=PRODUCER)
|
|
owner.update(cs, fault_active=reset, now_ns=now)
|
|
assert owner.allowed_latch and not owner._main_cycle_required
|
|
cs.cruiseState.available = False
|
|
owner.update(cs, fault_active=False, now_ns=now + 10_000_000)
|
|
assert not owner.allowed_latch
|
|
|
|
|
|
def test_registered_owner_matrix_is_exhaustive():
|
|
assert set(BRANDS) == set(vehicle._PORTS)
|