mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 17:13:54 +08:00
Preserve exact ASCM Volt steering receipts across receive ordering
This commit is contained in:
@@ -283,9 +283,9 @@ class SelfdriveD:
|
||||
source_ns = self.aol_car_state_log_ns
|
||||
source_current = bool(getattr(self, 'conditional_car_state_valid', False) and CS.canValid and not CS.canTimeout and
|
||||
0 < source_ns <= now_ns and now_ns - source_ns <= INTENT_MAX_AGE_NS)
|
||||
from openpilot.starpilot.car.gm.aol import sdgm_alpha_lateral_owner
|
||||
from openpilot.starpilot.car.gm.aol import steering_raw_projection_owner
|
||||
pair = companion.current(self.sm, source_ns=source_ns, now_ns=now_ns, drain=False,
|
||||
allow_raw_projection=sdgm_alpha_lateral_owner(self.CP)) if drained else None
|
||||
allow_raw_projection=steering_raw_projection_owner(self.CP)) if drained else None
|
||||
if not source_current:
|
||||
pair = None
|
||||
previous_axes = self.aol_axis_decision
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
"""Card's exact raw companion can precede the cached SubMaster projection."""
|
||||
from contextlib import ExitStack
|
||||
from unittest.mock import patch
|
||||
import pytest
|
||||
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
|
||||
from opendbc.car.gm.tests.test_ascm_intercept import params as ascm_params
|
||||
from opendbc.car.gm.values import CAR, apply_gm_auto_hold
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.cereal import log, messaging
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
from openpilot.selfdrive.car.cruise import VCruiseHelper
|
||||
from openpilot.selfdrive.car.tests.publication_fixture import initialize_publication_sources
|
||||
from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture
|
||||
from openpilot.starpilot.car.gm.steering_companion import SteeringCompanion
|
||||
|
||||
|
||||
@pytest.mark.parametrize('c9', [False, True])
|
||||
@pytest.mark.parametrize('radar', [False, True])
|
||||
@pytest.mark.parametrize('hold', [False, True])
|
||||
@pytest.mark.parametrize('full_op', [False, True])
|
||||
@pytest.mark.parametrize('condition', ['healthy', 'sm_fault', 'sm_invalid', 'sm_wrong_source',
|
||||
'sm_expired', 'sm_expired_fault', 'sm_expired_invalid', 'sm_future', 'missing_exact', 'raw_fault',
|
||||
'raw_invalid', 'raw_wrong_source', 'raw_future', 'raw_expired', 'source_invalid'])
|
||||
def test_ascm_exact_card_raw_companion_survives_older_cached_projection(c9, radar, hold, full_op, condition, prepared=False):
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
from openpilot.starpilot.aol.tests.test_gm_source_deadline import bind_steering_receipts
|
||||
actual_recv, actual_optional = messaging.recv_one, messaging.recv_one_or_none
|
||||
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
||||
with ExitStack() as cleanup, OpenpilotPrefix():
|
||||
cleanup.callback(case.doCleanups)
|
||||
messaging.reset_context()
|
||||
cp = ascm_params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, accelerator=not c9, radar=radar, alpha=True)
|
||||
apply_gm_auto_hold(cp, hold)
|
||||
if prepared:
|
||||
from openpilot.starpilot.vehicle_preferences import VehicleStartupPreferences
|
||||
VehicleStartupPreferences(volt_one_pedal=True, gm_auto_hold=True, volt_sng=True).prepare(cp)
|
||||
assert cp.safetyConfigs[0].safetyParam == 0xD114 and cp.flags == 16
|
||||
sd, _, step = case._authority_callers(full_op, cp=cp)
|
||||
from opendbc.car.gm.steering_authority import monitored_profile
|
||||
assert monitored_profile(sd.CP) and sd.CP.openpilotLongitudinalControl and not sd.CP.pcmCruise
|
||||
cp_before = sd.CP.to_dict()
|
||||
step = bind_steering_receipts(sd, step, cleanup)
|
||||
assert step(0)[0].lateralActive
|
||||
pm = messaging.PubMaster(['carState', 'carOutput', 'starpilotCarState'])
|
||||
sd.car_state_sock = messaging.sub_sock('carState', timeout=100)
|
||||
sd.steering_companion = SteeringCompanion(messaging.sub_sock('starpilotCarState', conflate=False))
|
||||
latest = messaging.sub_sock('starpilotCarState', conflate=True, timeout=100)
|
||||
card = Car.__new__(Car)
|
||||
card.CP, card.pm = sd.CP, pm
|
||||
initialize_publication_sources(card)
|
||||
authority = card.CI.CS.steering_authority
|
||||
card.sm = messaging.SubMaster(['carControl'], ignore_avg_freq=['carControl'])
|
||||
neutral = messaging.new_message('carControl', valid=True, logMonoTime=10_000_000_000)
|
||||
card.sm.update_msgs(10., [neutral.as_reader()])
|
||||
card.sm.frame = 1
|
||||
card.car_params_published = True
|
||||
card.last_actuators_output = car.CarControl.Actuators.new_message()
|
||||
card.slc_replay = card.curve_replay = card.aol_replay = False
|
||||
card.can_rcv_cum_timeout_counter = 0
|
||||
card.rk = Ratekeeper(100., print_delay_threshold=None)
|
||||
card.v_cruise_helper = VCruiseHelper(card.CP)
|
||||
source = 10_010_000_000
|
||||
now = source + (31_000_000 if condition == 'raw_expired' else 4_000_000)
|
||||
older = now - (33_000_000 if condition.startswith('sm_expired') else 10_000_000)
|
||||
original_message = messaging.new_message
|
||||
|
||||
def publish(stamp, *, fault=False, valid=True, enabled=True):
|
||||
authority.enabled, authority.latched = enabled, fault
|
||||
cs = fixture.car_state()
|
||||
cs.canValid = valid
|
||||
cs.cruiseState.available = True
|
||||
with patch('openpilot.selfdrive.car.card.messaging.new_message', side_effect=lambda *a, **kw:
|
||||
original_message(*a, **dict(kw, logMonoTime=stamp))):
|
||||
card.state_publish(cs, None)
|
||||
|
||||
publish(older, fault=condition in ('sm_fault','sm_expired_fault'), valid=condition not in ('sm_invalid','sm_expired_invalid'))
|
||||
assert actual_recv(sd.car_state_sock).logMonoTime == older
|
||||
cached = actual_recv(latest).as_builder()
|
||||
assert cached.logMonoTime == older
|
||||
if condition == 'sm_wrong_source':
|
||||
cached.starpilotCarState.sourceCarStateMonoTime -= 1
|
||||
if condition == 'sm_future':
|
||||
cached.logMonoTime = now + 1
|
||||
cached.starpilotCarState.sourceCarStateMonoTime = now + 1
|
||||
|
||||
def receive_first(sock):
|
||||
publish(source, fault=condition == 'raw_fault', enabled=condition != 'missing_exact', valid=condition != 'source_invalid')
|
||||
first = actual_recv(sock)
|
||||
assert first.logMonoTime == source
|
||||
return first
|
||||
|
||||
def receive_cached(_timeout):
|
||||
sd.sm.update_msgs(now / 1e9, [cached.as_reader()])
|
||||
|
||||
def receive_raw(sock):
|
||||
if sock is sd.car_state_sock:
|
||||
return None
|
||||
message = actual_optional(sock)
|
||||
if message is not None and message.logMonoTime == source and condition in ('raw_invalid', 'raw_wrong_source', 'raw_future'):
|
||||
message = message.as_builder()
|
||||
if condition == 'raw_invalid':
|
||||
message.valid = False
|
||||
elif condition == 'raw_wrong_source':
|
||||
message.starpilotCarState.sourceCarStateMonoTime -= 1
|
||||
else:
|
||||
message.logMonoTime = now + 1
|
||||
message.starpilotCarState.sourceCarStateMonoTime = now + 1
|
||||
return message.as_reader()
|
||||
return message
|
||||
|
||||
sd.sm.update.side_effect = receive_cached
|
||||
with patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one', side_effect=receive_first), \
|
||||
patch('openpilot.starpilot.car.gm.steering_companion.messaging.recv_one_or_none', side_effect=receive_raw), \
|
||||
patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=now):
|
||||
axis, _, cc = step(1)
|
||||
assert sd.CP.to_dict() == cp_before
|
||||
healthy = condition in ('healthy', 'sm_expired')
|
||||
assert axis.sourceCarStateMonoTime == (0 if condition in ('raw_expired', 'source_invalid') else source)
|
||||
assert bool(axis.lateralActive) == healthy and bool(cc.latActive) == healthy
|
||||
assert bool(cc.longActive) == (healthy and full_op)
|
||||
assert (log.OnroadEvent.EventName.controlsMismatch in sd.events.names) == (not healthy)
|
||||
if healthy:
|
||||
assert sd.sm.logMonoTime['starpilotCarState'] == older
|
||||
assert sd.steering_companion.samples[source][0] == source
|
||||
assert axis.validUntilMonoTime == axis.observedMonoTime + 30_000_000
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize('c9', [False, True])
|
||||
@pytest.mark.parametrize('radar', [False, True])
|
||||
@pytest.mark.parametrize('hold', [False, True])
|
||||
@pytest.mark.parametrize('alternative_experience', [32, 96])
|
||||
def test_raw_projection_owner_keeps_exact_nonpedal_scope(c9, radar, hold, alternative_experience):
|
||||
from openpilot.starpilot.car.gm.aol import steering_raw_projection_owner
|
||||
cp = ascm_params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, accelerator=not c9, radar=radar, alpha=True)
|
||||
cp.alternativeExperience = alternative_experience
|
||||
apply_gm_auto_hold(cp, hold)
|
||||
original = cp.to_dict()
|
||||
assert steering_raw_projection_owner(cp) == (alternative_experience == 32)
|
||||
for fault in ('unknown_ae', 'passive', 'dashcam', 'not_car', 'wrong_word', 'wrong_model', 'wrong_brand', 'wrong_identity', 'extra_panda'):
|
||||
invalid = cp.as_reader().as_builder()
|
||||
if fault == 'unknown_ae':
|
||||
invalid.alternativeExperience = 0xFF
|
||||
elif fault == 'passive':
|
||||
invalid.passive = True
|
||||
elif fault == 'dashcam':
|
||||
invalid.dashcamOnly = True
|
||||
elif fault == 'not_car':
|
||||
invalid.notCar = True
|
||||
elif fault == 'wrong_word':
|
||||
invalid.safetyConfigs[0].safetyParam = 0xFFFF
|
||||
elif fault == 'wrong_model':
|
||||
invalid.safetyConfigs[0].safetyModel = 'silent'
|
||||
elif fault == 'wrong_brand':
|
||||
invalid.brand = 'toyota'
|
||||
elif fault == 'wrong_identity':
|
||||
invalid.carFingerprint = 'UNKNOWN_VOLT'
|
||||
else:
|
||||
invalid.safetyConfigs = [invalid.safetyConfigs[0].to_dict()] * 2
|
||||
assert not steering_raw_projection_owner(invalid), fault
|
||||
ordinary = cp.as_reader().as_builder()
|
||||
ordinary.alternativeExperience = 0
|
||||
assert steering_raw_projection_owner(ordinary)
|
||||
assert cp.to_dict() == original
|
||||
|
||||
|
||||
@pytest.mark.parametrize('topology', ['camera', 'gateway', 'ascm', 'sdgm'])
|
||||
@pytest.mark.parametrize('alpha', [False, True])
|
||||
@pytest.mark.parametrize('be', [False, True])
|
||||
@pytest.mark.parametrize('release', [False, True])
|
||||
def test_raw_projection_owner_excludes_separate_pedal_profiles(topology, alpha, be, release):
|
||||
from opendbc.car.gm.tests import test_volt_transitions as factory
|
||||
from openpilot.starpilot.car.gm.aol import steering_raw_projection_owner
|
||||
options = {'alpha': alpha, 'be': be, 'release': release}
|
||||
if topology in ('ascm', 'sdgm'):
|
||||
options['sascm'] = True
|
||||
cp = getattr(factory, 'volt_' + topology + '_pedal_params')(**options)
|
||||
cp.alternativeExperience = 32
|
||||
assert not steering_raw_projection_owner(cp)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sdgm', [False, True])
|
||||
@pytest.mark.parametrize('alpha,sascm', [(False, True), (True, False)])
|
||||
def test_raw_projection_owner_excludes_stock_and_missing_sascm(sdgm, alpha, sascm):
|
||||
from openpilot.starpilot.car.gm.aol import steering_raw_projection_owner
|
||||
cp = sdgm_params(alpha=alpha, sascm=sascm) if sdgm else ascm_params(CAR.CHEVROLET_VOLT_ASCM, alpha=alpha, sascm=sascm)
|
||||
cp.alternativeExperience = 32
|
||||
assert not steering_raw_projection_owner(cp)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('full_op', [False, True])
|
||||
@pytest.mark.parametrize('condition', ['healthy', 'sm_expired', 'sm_expired_fault', 'raw_fault'])
|
||||
def test_recorded_sascm_one_pedal_hold_configuration(full_op, condition):
|
||||
test_ascm_exact_card_raw_companion_survives_older_cached_projection(False, True, True, full_op, condition, prepared=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sdgm', [False, True])
|
||||
@pytest.mark.parametrize('c9', [False, True])
|
||||
@pytest.mark.parametrize('initial_active', [False, True])
|
||||
@pytest.mark.parametrize('condition', ['sm_expired', 'sm_expired_fault', 'raw_fault', 'source_invalid', 'raw_expired'])
|
||||
def test_normal_control_receipts_with_aol_disabled(sdgm, c9, initial_active, condition):
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
from openpilot.selfdrive.controls.controlsd import Controls
|
||||
from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD
|
||||
from openpilot.selfdrive.selfdrived.events import Events
|
||||
from openpilot.selfdrive.selfdrived.state import StateMachine
|
||||
from openpilot.starpilot.aol.runtime import AxisDecision
|
||||
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
|
||||
|
||||
with OpenpilotPrefix():
|
||||
cp = sdgm_params(alpha=True, brake_c9=c9) if sdgm else ascm_params(
|
||||
CAR.CHEVROLET_VOLT_ASCM, sascm=True, accelerator=not c9, radar=True, alpha=True)
|
||||
assert cp.alternativeExperience == 0
|
||||
saved = Params()
|
||||
saved.put_bool('OpenpilotEnabledToggle', True, block=True)
|
||||
saved.put_bool('AlwaysOnLateral', False, block=True)
|
||||
saved.put('CarParams', cp.to_bytes(), block=True)
|
||||
controls = Controls()
|
||||
assert not controls.aol_replay
|
||||
pm = messaging.PubMaster(['carState', 'carOutput', 'starpilotCarState'])
|
||||
cs_sock = messaging.sub_sock('carState', timeout=100)
|
||||
raw_sock = messaging.sub_sock('starpilotCarState', conflate=False)
|
||||
cached_sock = messaging.sub_sock('starpilotCarState', conflate=True, timeout=100)
|
||||
card = Car.__new__(Car)
|
||||
card.CP, card.pm = cp, pm
|
||||
initialize_publication_sources(card)
|
||||
card.sm = messaging.SubMaster(['carControl'], ignore_avg_freq=['carControl'])
|
||||
card.sm.update_msgs(10.0, [messaging.new_message('carControl', valid=True, logMonoTime=10_000_000_000).as_reader()])
|
||||
card.sm.frame = 1
|
||||
card.car_params_published = True
|
||||
card.last_actuators_output = car.CarControl.Actuators.new_message()
|
||||
card.slc_replay = card.curve_replay = card.aol_replay = False
|
||||
card.can_rcv_cum_timeout_counter = 0
|
||||
card.rk = Ratekeeper(100.0, print_delay_threshold=None)
|
||||
card.v_cruise_helper = VCruiseHelper(cp)
|
||||
now = 10_100_000_000
|
||||
source = now - (31_000_000 if condition == 'raw_expired' else 4_000_000)
|
||||
old = now - 33_000_000
|
||||
actual_message = messaging.new_message
|
||||
|
||||
def publish(stamp, *, fault=False, valid=True):
|
||||
cs = fixture.car_state()
|
||||
cs.canValid = valid
|
||||
card.CI.CS.steering_authority.latched = fault
|
||||
with patch('openpilot.selfdrive.car.card.messaging.new_message', side_effect=lambda *a, **kw: actual_message(*a, **dict(kw, logMonoTime=stamp))):
|
||||
card.state_publish(cs, None)
|
||||
received = messaging.recv_one(cs_sock)
|
||||
assert received.logMonoTime == stamp
|
||||
return received
|
||||
|
||||
publish(old, fault=condition == 'sm_expired_fault')
|
||||
cached = messaging.recv_one(cached_sock)
|
||||
sm = messaging.SubMaster(['starpilotCarState'], ignore_avg_freq=['starpilotCarState'])
|
||||
sm.update_msgs(now / 1e9, [cached])
|
||||
latest = publish(source, fault=condition == 'raw_fault', valid=condition != 'source_invalid')
|
||||
sd = SelfdriveD.__new__(SelfdriveD)
|
||||
sd.CP, sd.sm = cp.as_reader(), sm
|
||||
sd.active = initial_active
|
||||
sd.aol_axis_decision = AxisDecision()
|
||||
sd.aol_car_state_log_ns = source
|
||||
sd.conditional_car_state_valid = True
|
||||
sd.steering_authority_unavailable = False
|
||||
sd.steering_companion = SteeringCompanion(raw_sock)
|
||||
sd.events = Events()
|
||||
with patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', False), \
|
||||
patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=now):
|
||||
sd._update_steering_authority(latest.carState)
|
||||
healthy = condition == 'sm_expired'
|
||||
machine = StateMachine()
|
||||
machine.state = log.SelfdriveState.OpenpilotState.enabled if initial_active else log.SelfdriveState.OpenpilotState.disabled
|
||||
enabled, active = machine.update(sd.events)
|
||||
for tick in range(10):
|
||||
feed(controls, now - (10 - tick) * 10_000_000, tick, enabled=False, active=False)
|
||||
feed(controls, now, 10, enabled=enabled, active=active)
|
||||
controls.sm.update_msgs(now / 1e9, [latest])
|
||||
with patch('openpilot.selfdrive.controls.controlsd.time.monotonic_ns', return_value=now):
|
||||
command, _ = controls.state_control()
|
||||
assert command.latActive == command.longActive == (healthy and initial_active)
|
||||
assert active == enabled == (healthy and initial_active)
|
||||
assert not controls.CP.alternativeExperience and not saved.get_bool('AlwaysOnLateral')
|
||||
if initial_active:
|
||||
assert (log.OnroadEvent.EventName.controlsMismatch in sd.events.names) == (not healthy)
|
||||
@@ -25,7 +25,10 @@ def test_long_isolated_lag_recovery_keeps_only_existing_intent(c9, alpha):
|
||||
axis, _, command = drive(tick, cs, allowed=tick == 2, native_request_lat=tick == 2)
|
||||
assert not axis.lateralActive and not axis.longitudinalActive
|
||||
assert not command.latActive and not command.longActive
|
||||
assert str(axis.faultReason) == 'critical'
|
||||
assert str(axis.faultReason) == 'availabilityPause'
|
||||
assert axis.faultSessionId == sd.aol_session_id and not axis.nativeAcknowledged
|
||||
assert axis.sourceCarStateMonoTime == BASE + tick * 10_000_000
|
||||
assert sd.aol_published_fault_proof is None
|
||||
assert not axis.desiredLateral and not axis.desiredLongitudinal
|
||||
assert axis.validUntilMonoTime == axis.observedMonoTime + 30_000_000
|
||||
if event_ns is None:
|
||||
@@ -64,7 +67,9 @@ def test_stock_pcm_off_lag_never_exempts_other_rejection(bad):
|
||||
assert card.aol_card_intent.allowed_latch and not command.latActive
|
||||
cs = parse(3, 'cancel' if bad == 'cancel' else 'none', main=bad != 'main_off')
|
||||
cs.doorOpen = bad == 'door'
|
||||
options = {'critical_event': log.OnroadEvent.EventName.processNotRunning} if bad == 'other_fault' else {}
|
||||
# processNotRunning is a SOFT availability pause. This negative case must
|
||||
# inject the actual immediate fault whose disarm behavior it protects.
|
||||
options = {'critical_event': log.OnroadEvent.EventName.controlsMismatch} if bad == 'other_fault' else {}
|
||||
with mock.patch.object(sd.rk.avg_dt, 'get_average', return_value=.02):
|
||||
axis, _, command = drive(3, cs, allowed=False, native_request_lat=False, native_valid=bad != 'native_invalid', **options)
|
||||
if bad == 'epoch':
|
||||
@@ -73,4 +78,10 @@ def test_stock_pcm_off_lag_never_exempts_other_rejection(bad):
|
||||
sd.pm.messages['aolAxisState'] = wire.as_reader()
|
||||
deliver_card(card, sd, controls, cs, 3)
|
||||
assert not command.latActive and not command.longActive
|
||||
assert not card.aol_card_intent.allowed_latch
|
||||
if bad == 'cancel':
|
||||
# GM's ordinary Cancel withdraws LONG; MAIN still owns lateral intent.
|
||||
assert card.aol_card_intent.allowed_latch and not card.aol_card_intent._main_cycle_required
|
||||
assert any(str(event.type) == 'cancel' and event.pressed for event in cs.buttonEvents)
|
||||
assert not axis.longitudinalActive and not sd.events.contains('enable')
|
||||
else:
|
||||
assert not card.aol_card_intent.allowed_latch
|
||||
|
||||
@@ -58,7 +58,8 @@ def test_cold_main_waits_for_host_readiness_but_keeps_real_faults(event):
|
||||
original_events = sd.update_events
|
||||
axis, state, command = drive(0, observed_state=cs, native_request_lat=False, native_request_long=False,
|
||||
allowed=False, long_allowed=False, critical_event=event)
|
||||
temporary = event in (EventName.commIssue, EventName.commIssueAvgFreq, EventName.locationdTemporaryError)
|
||||
temporary = event != EventName.controlsMismatch
|
||||
assert any(e.name == event and (e.softDisable if temporary else e.immediateDisable) for e in sd.events.to_msg())
|
||||
assert str(axis.faultReason) == ('availabilityPause' if temporary else 'critical')
|
||||
assert not command.latActive and not command.longActive and not state.enabled
|
||||
deliver_card(card, sd, controls, cs, 0)
|
||||
|
||||
@@ -34,11 +34,14 @@ def test_stock_lat_intent_fresh_lag_then_source_gap_then_recovery(family):
|
||||
with mock.patch.object(sd.rk.avg_dt, 'get_average', return_value=0.02):
|
||||
lag, state, command = drive(3, state_stamp=first, **settings('lateral_only', first, 4))
|
||||
assert not state.enabled and not state.active and not command.latActive and not command.longActive
|
||||
assert str(lag.faultReason) == 'critical' and lag.sourceCarStateMonoTime == first
|
||||
assert str(lag.faultReason) == 'availabilityPause' and lag.sourceCarStateMonoTime == first
|
||||
assert not lag.lateralActive and not lag.longitudinalActive and not lag.nativeAcknowledged
|
||||
witness = sd.aol_published_fault_proof
|
||||
assert witness is not None and len(witness) == 9 and not any(witness[4:])
|
||||
assert witness[0] == sd.aol_session_id and witness[1] == 'critical' and witness[2] == lag.faultEventMonoTime
|
||||
assert not lag.desiredLateral and not lag.desiredLongitudinal
|
||||
assert lag.faultSessionId == sd.aol_session_id and lag.faultEventMonoTime == first
|
||||
assert lag.validUntilMonoTime == lag.observedMonoTime + 30_000_000
|
||||
# SOFT availability is not a fabricated critical/user-disable witness.
|
||||
assert sd.aol_published_fault_proof is None
|
||||
assert sd.aol_published_stock_lateral_proof is None
|
||||
users = [e for e in sd.events.to_msg() if e.userDisable]
|
||||
expected = log.OnroadEvent.EventName.cruiseDisabled if family == 'honda' else log.OnroadEvent.EventName.pcmDisable
|
||||
assert users and all(e.name == expected and not e.softDisable and not e.immediateDisable for e in users)
|
||||
|
||||
@@ -26,7 +26,8 @@ def test_actual_isolated_lag_keeps_only_existing_card_intent(full_op, gas, order
|
||||
axis, state, command = drive(2, gas=gas, native_request_long=full_op)
|
||||
# Already engaged ordinary axes follow the normal SOFT countdown with
|
||||
# fresh inputs/native grants; independent AOL still withdraws commands.
|
||||
assert str(axis.faultReason) == ('none' if full_op else 'critical')
|
||||
assert str(axis.faultReason) == ('none' if full_op else 'availabilityPause')
|
||||
assert sd.aol_published_fault_proof is None
|
||||
assert axis.lateralActive == axis.longitudinalActive == full_op
|
||||
assert command.latActive == command.longActive == full_op
|
||||
assert axis.desiredLateral == axis.desiredLongitudinal == axis.nativeAcknowledged == full_op
|
||||
@@ -41,13 +42,16 @@ def test_actual_isolated_lag_keeps_only_existing_card_intent(full_op, gas, order
|
||||
assert proof.aolAxisState.faultEventMonoTime == event.logMonoTime
|
||||
deliveries = [{'axis': proof}, {'event': event}] if order == 'axis_first' else \
|
||||
[{'event': event}, {'axis': proof}] if order == 'event_first' else [{'axis': proof, 'event': event}]
|
||||
# Independent critical joins retain only intent under the fixed join.
|
||||
# Independent warning joins retain only intent under the fixed join.
|
||||
# Ordinary fresh proofs retain their actual current acknowledged axes.
|
||||
for index, delivery in enumerate(deliveries):
|
||||
latched = receive(10_020_000_000 + index * 100_000, **delivery)
|
||||
if not full_op and order == 'axis_first' and index == 0:
|
||||
assert feedback.pending_event == proof.aolAxisState.faultEventMonoTime
|
||||
assert feedback.awaiting
|
||||
anchor = feedback.pending_event
|
||||
assert receive(10_020_050_000, axis=proof)
|
||||
assert feedback.pending_event == anchor and proof.aolAxisState.validUntilMonoTime == 10_050_000_000
|
||||
assert latched and not feedback.failed
|
||||
observed = (feedback.lateral_baseline_ns, feedback.lateral_source_ns,
|
||||
feedback.lateral_native_ns, feedback.healthy_feedback_ns, feedback.healthy_native_ns)
|
||||
@@ -122,8 +126,14 @@ def test_temporary_lag_never_masks_real_fault_or_invalid_proof(fault):
|
||||
card.aol_card_intent.settings = replace(card.aol_card_intent.settings, enabled=False)
|
||||
now = 10_020_000_000 if fault != 'axis_deadline' else 10_050_000_001
|
||||
assert not command.latActive and not command.longActive
|
||||
assert not receive(now, axis=proof.as_reader(), event=event.as_reader(), state=state)
|
||||
assert not card.aol_card_intent.allowed_latch
|
||||
retained = receive(now, axis=proof.as_reader(), event=event.as_reader(), state=state)
|
||||
soft_only = fault in ('process_failure', 'overheat')
|
||||
assert retained == soft_only and card.aol_card_intent.allowed_latch == soft_only
|
||||
if soft_only:
|
||||
assert str(proof.aolAxisState.faultReason) == 'availabilityPause'
|
||||
assert not axis.lateralActive and not axis.longitudinalActive and not axis.nativeAcknowledged
|
||||
assert not sd.aol_authority_lost and sd.aol_published_fault_proof is None
|
||||
assert card.aol_transport_feedback.ordinary_warning_qualified
|
||||
|
||||
|
||||
def test_persistent_lag_keeps_latch_but_ordinary_countdown_still_disables():
|
||||
@@ -144,7 +154,8 @@ def test_persistent_lag_keeps_latch_but_ordinary_countdown_still_disables():
|
||||
ordinary_active = tick < 302
|
||||
assert state.enabled == state.active == ordinary_active
|
||||
assert sd.state_machine.soft_disable_timer == max(0, 302 - tick)
|
||||
assert str(axis.faultReason) == ('none' if ordinary_active else 'critical')
|
||||
assert str(axis.faultReason) == ('none' if ordinary_active else 'availabilityPause')
|
||||
assert sd.aol_published_fault_proof is None
|
||||
assert axis.desiredLateral == axis.desiredLongitudinal == axis.nativeAcknowledged == ordinary_active
|
||||
assert axis.lateralActive == command.latActive == ordinary_active
|
||||
# LONG never activates: this fixture deliberately keeps native's
|
||||
@@ -194,7 +205,8 @@ def test_persistent_lag_does_not_mask_native_controls_mismatch():
|
||||
assert not card.aol_card_intent.allowed_latch
|
||||
|
||||
|
||||
@pytest.mark.parametrize('failure', ['missing_event', 'hard_event', 'wrong_event', 'session', 'inventory'])
|
||||
@pytest.mark.parametrize('failure', ['missing_event', 'hard_event', 'wrong_event', 'session', 'inventory',
|
||||
'producer', 'epoch', 'source_stale', 'new_occurrence'])
|
||||
def test_axis_first_critical_join_never_masks_missing_or_hard_occurrence(failure):
|
||||
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
||||
with ExitStack() as cleanup:
|
||||
@@ -204,6 +216,8 @@ def test_axis_first_critical_join_never_masks_missing_or_hard_occurrence(failure
|
||||
with mock.patch.object(type(sd.rk), 'lagging', new_callable=mock.PropertyMock, return_value=True):
|
||||
axis, _, command = drive(2, native_request_long=False)
|
||||
proof = sd.pm.messages['aolAxisState'].as_builder()
|
||||
assert str(proof.aolAxisState.faultReason) == 'availabilityPause'
|
||||
assert sd.aol_published_fault_proof is None
|
||||
assert receive(10_020_000_000, axis=proof.as_reader())
|
||||
anchor = card.aol_transport_feedback.pending_event
|
||||
assert anchor == proof.aolAxisState.faultEventMonoTime
|
||||
@@ -224,6 +238,16 @@ def test_axis_first_critical_join_never_masks_missing_or_hard_occurrence(failure
|
||||
elif failure == 'session':
|
||||
proof.aolAxisState.sessionId = proof.aolAxisState.faultSessionId = 'other-session'
|
||||
delivery = {'axis': proof.as_reader(), 'event': event.as_reader()}
|
||||
elif failure in ('producer', 'epoch', 'source_stale', 'new_occurrence'):
|
||||
if failure == 'producer':
|
||||
proof.aolAxisState.intentProducerSessionId = 'other-card'
|
||||
elif failure == 'epoch':
|
||||
proof.aolAxisState.intentIntegrityEpoch += 1
|
||||
elif failure == 'source_stale':
|
||||
proof.aolAxisState.sourceCarStateMonoTime -= 30_000_001
|
||||
else:
|
||||
proof.aolAxisState.faultEventMonoTime += 1
|
||||
delivery = {'axis': proof.as_reader(), 'event': event.as_reader()}
|
||||
else:
|
||||
from dataclasses import replace
|
||||
from openpilot.cereal import messaging
|
||||
|
||||
@@ -100,7 +100,10 @@ def test_parsed_pedal_lag_keeps_intent_without_renewing_authority(pedal, order,
|
||||
cs = parse(2, pedal)
|
||||
with mock.patch.object(sd.rk.avg_dt, 'get_average', return_value=.02):
|
||||
axis, _, command = drive(2, cs)
|
||||
assert not command.latActive and not command.longActive and str(axis.faultReason) == 'critical'
|
||||
assert not command.latActive and not command.longActive and str(axis.faultReason) == 'availabilityPause'
|
||||
assert not axis.lateralActive and not axis.longitudinalActive and not axis.nativeAcknowledged
|
||||
assert axis.faultSessionId == sd.aol_session_id and axis.faultEventMonoTime == BASE + 20_000_000
|
||||
assert sd.aol_published_fault_proof is None
|
||||
assert not axis.desiredLateral and not axis.desiredLongitudinal
|
||||
proof, event = sd.pm.messages['aolAxisState'], sd.pm.messages['onroadEvents']
|
||||
if order != 'together':
|
||||
@@ -183,8 +186,19 @@ def test_pedal_overlap_never_bypasses_other_rejection(bad):
|
||||
setattr(cs, {'can_invalid': 'canValid', 'permanent_eps': 'steerFaultPermanent',
|
||||
'door': 'doorOpen', 'belt': 'seatbeltUnlatched'}[bad], bad != 'can_invalid')
|
||||
sd.pm.messages['aolAxisState'] = proof.as_reader()
|
||||
baseline = (card.aol_transport_feedback.lateral_baseline_ns, card.aol_transport_feedback.lateral_source_ns,
|
||||
card.aol_transport_feedback.lateral_native_ns)
|
||||
deliver_card(card, sd, controls, cs, 2)
|
||||
assert not card.aol_card_intent.allowed_latch
|
||||
intent_only = bad in ('commIssue', 'processNotRunning', 'native_long', 'belt')
|
||||
assert card.aol_card_intent.allowed_latch == intent_only
|
||||
if intent_only:
|
||||
# Qualified SOFT withdrawal retains intent, never an output permission.
|
||||
assert str(proof.aolAxisState.faultReason) == 'availabilityPause'
|
||||
assert not proof.aolAxisState.lateralActive and not proof.aolAxisState.longitudinalActive
|
||||
assert not proof.aolAxisState.nativeAcknowledged
|
||||
assert not card.sm['carControl'].latActive and not card.sm['carControl'].longActive
|
||||
assert baseline == (card.aol_transport_feedback.lateral_baseline_ns, card.aol_transport_feedback.lateral_source_ns,
|
||||
card.aol_transport_feedback.lateral_native_ns)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pedal', ['brake', 'regen'])
|
||||
|
||||
@@ -301,6 +301,7 @@ class TransportPauseFeedback:
|
||||
self.source_retention_started_ns = 0
|
||||
self.ordinary_warning_retained = False
|
||||
self.ordinary_warning_qualified = False
|
||||
self.ordinary_warning_pending_baseline = None
|
||||
self.healthy_feedback_ns = 0
|
||||
self.healthy_native_ns = 0
|
||||
self.healthy_inventory = None
|
||||
@@ -366,7 +367,9 @@ class TransportPauseFeedback:
|
||||
from openpilot.starpilot.nostalgia import physical_cancel
|
||||
from opendbc.car import structs
|
||||
|
||||
if (not latched or self.failed or self.paused or self.pending_event or self.retention_started_ns or
|
||||
pending_baseline = self.ordinary_warning_pending_baseline
|
||||
pending = bool(pending_baseline is not None and self.pending_event)
|
||||
if (not latched or self.failed or self.paused or self.pending_event and not pending or self.retention_started_ns or
|
||||
not self.session or not self.lateral_baseline_ns or not self.lateral_inventory or
|
||||
not policy_for(cp).intent_supported or
|
||||
policy_for(cp).cruise_main_required and not cs.cruiseState.available or
|
||||
@@ -389,7 +392,8 @@ class TransportPauseFeedback:
|
||||
return False
|
||||
warning = any(event.softDisable for event in events)
|
||||
recovering = bool(getattr(self, 'ordinary_warning_retained', False) and not warning)
|
||||
if not warning and not recovering:
|
||||
if pending and (axis.faultEventMonoTime != self.pending_event or
|
||||
not 0 < self.pending_event <= now_ns or now_ns - self.pending_event > AXIS_MAX_AGE_NS):
|
||||
return False
|
||||
stock_user = stock_lateral_user_event(cp)
|
||||
# Physical withdrawals still go through the family button/pedal owner.
|
||||
@@ -400,7 +404,19 @@ class TransportPauseFeedback:
|
||||
cp.brand == 'gm' and 0 < self.gm_withdrawal_ns == event_ns <= now_ns and
|
||||
now_ns - event_ns <= SAFETY_MAX_AGE_NS)) for event in events):
|
||||
return False
|
||||
prior = self.signature
|
||||
prior = pending_baseline if pending else self.signature
|
||||
# The typed SOFT withdrawal may precede its event socket. Hold only intent
|
||||
# under the fixed original 30ms join until that exact event arrives.
|
||||
# No duplicate or newer withdrawal can move the pending occurrence.
|
||||
axis_first = bool(str(axis.faultReason) == 'availabilityPause' and
|
||||
not (axis.lateralActive or axis.longitudinalActive or axis.desiredLateral or
|
||||
axis.desiredLongitudinal or axis.nativeAcknowledged) and
|
||||
prior is not None and prior[5] == 'none' and prior[9] and prior[11] and prior[13] and
|
||||
prior[14:16] == (producer, self.availability_epoch) and
|
||||
0 < prior[8] <= now_ns and (pending or now_ns <= prior[4] and now_ns - prior[8] <= AXIS_MAX_AGE_NS) and
|
||||
event_ns < axis.faultEventMonoTime <= stamp and now_ns - axis.faultEventMonoTime <= AXIS_MAX_AGE_NS)
|
||||
if not warning and not recovering and not axis_first:
|
||||
return False
|
||||
active_join = bool(warning and prior is not None and str(axis.faultReason) == 'none' and
|
||||
axis.lateralActive and axis.desiredLateral and axis.nativeAcknowledged and
|
||||
prior[9] and prior[11] and prior[13] and prior[14:16] == (producer, self.availability_epoch) and
|
||||
@@ -410,16 +426,27 @@ class TransportPauseFeedback:
|
||||
not axis.desiredLongitudinal and
|
||||
(warning and str(axis.faultReason) == 'availabilityPause' and axis.faultEventMonoTime == event_ns <= stamp or
|
||||
recovering and str(axis.faultReason) == 'none'))
|
||||
if not active_join and not withdrawn:
|
||||
if not active_join and not withdrawn and not axis_first:
|
||||
return False
|
||||
if pending and not axis_first and not (withdrawn and warning and event_ns == self.pending_event):
|
||||
return False
|
||||
native = _configured_transport_native(sm, cp, self.session, now_ns,
|
||||
allow_restricted_lateral=withdrawn or bool(cs.steerFaultTemporary),
|
||||
require_longitudinal=not withdrawn)
|
||||
allow_restricted_lateral=withdrawn or axis_first or bool(cs.steerFaultTemporary),
|
||||
require_longitudinal=not (withdrawn or axis_first))
|
||||
if (native is None or not native.pandaInventory or
|
||||
(native.pandaSerial, tuple(slot.hardwareSerial for slot in native.pandaInventory)) != self.lateral_inventory or
|
||||
native.observedMonoTime < self.lateral_native_ns or
|
||||
active_join and not (native.requestedLateral and native.lateralAllowed)):
|
||||
return False
|
||||
if axis_first:
|
||||
if not pending:
|
||||
self.pending_event = int(axis.faultEventMonoTime)
|
||||
self.ordinary_warning_pending_baseline = prior
|
||||
self.awaiting = True
|
||||
return True
|
||||
if pending:
|
||||
self.pending_event = 0
|
||||
self.ordinary_warning_pending_baseline = None
|
||||
self.ordinary_warning_retained = True
|
||||
return True
|
||||
|
||||
@@ -879,7 +906,8 @@ class TransportPauseFeedback:
|
||||
# Unmatched joins retain pending_event; every other critical path fails
|
||||
# and clears the LAT-only baseline before changing the signature.
|
||||
self.lateral_only_baseline and not self.failed and not self.pending_event and
|
||||
previous[5] == 'critical' and previous[1] == self.sequence and not any(previous[9:14]) and
|
||||
(previous[5] == 'critical' or previous[5] == 'availabilityPause' and self.ordinary_warning_retained) and
|
||||
previous[1] == self.sequence and not any(previous[9:14]) and
|
||||
0 < previous[6] <= previous[2] <= axis.faultEventMonoTime and
|
||||
0 < self.lateral_source_ns <= previous[8] <= previous[2] and
|
||||
previous[2] - previous[8] <= AXIS_MAX_AGE_NS and
|
||||
@@ -1006,7 +1034,8 @@ class TransportPauseFeedback:
|
||||
output_missing = not (sm.seen['aolAxisState'] and sm.valid['aolAxisState'] and sm.alive['aolAxisState'])
|
||||
# The same exclusive matched-lag signature invariant as typed retention:
|
||||
# unknown critical joins keep pending_event; other critical paths fail.
|
||||
matched_lag = bool(str(axis.faultReason) == 'critical' and signature == self.signature and
|
||||
matched_lag = bool((str(axis.faultReason) == 'critical' or
|
||||
str(axis.faultReason) == 'availabilityPause' and self.ordinary_warning_retained) and signature == self.signature and
|
||||
self.lateral_only_baseline and not self.failed and not self.pending_event and
|
||||
signature[0] == self.session and signature[7] == self.session and signature[1] == self.sequence and
|
||||
signature[14] == producer == self.availability_producer and signature[15] == self.availability_epoch and
|
||||
@@ -1167,6 +1196,7 @@ class TransportPauseFeedback:
|
||||
axis = sm['aolAxisState']
|
||||
if not latched or cp_identity(cp) != self.cp or self.session and str(axis.sessionId) != self.session:
|
||||
self.ordinary_warning_retained = False
|
||||
self.ordinary_warning_pending_baseline = None
|
||||
self.temporary_eps_event_ns = 0
|
||||
self.ccnc_main_off_source_ns = 0
|
||||
self.cold_park_waiting = False
|
||||
|
||||
@@ -48,6 +48,12 @@ def sdgm_alpha_lateral_owner(cp) -> bool:
|
||||
return bool(qualified_gm(cp) and aol_enabled(cp) and is_volt_sdgm_accepted_envelope(cp))
|
||||
|
||||
|
||||
def steering_raw_projection_owner(cp) -> bool:
|
||||
"""Qualified non-pedal Volt owners consume their exact raw steering receipt."""
|
||||
from opendbc.car.gm.steering_authority import monitored_profile
|
||||
return bool(qualified_gm(cp) and monitored_profile(cp))
|
||||
|
||||
|
||||
def sdgm_alpha_pedal_withdrawal(cp, cs) -> bool:
|
||||
"""Physical LONG withdrawal for the 2019 SASCM Alpha configuration."""
|
||||
from opendbc.car.gm.values import is_volt_sdgm_accepted_envelope
|
||||
@@ -192,7 +198,7 @@ def cold_main_readiness(cp, cs, native, events, *, recovering=False):
|
||||
"""A MAIN request may wait for host startup without acquiring either axis."""
|
||||
from openpilot.cereal import log
|
||||
names = (log.OnroadEvent.EventName.commIssue, log.OnroadEvent.EventName.commIssueAvgFreq,
|
||||
log.OnroadEvent.EventName.locationdTemporaryError)
|
||||
log.OnroadEvent.EventName.locationdTemporaryError, log.OnroadEvent.EventName.processNotRunning)
|
||||
faults = [event for event in events if event.softDisable or event.immediateDisable]
|
||||
host_wait = not faults if recovering else bool(faults and
|
||||
all(event.softDisable and not event.immediateDisable and event.name in names for event in faults))
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "4e1b9257513a6301cec583c3e21e83ebbdd48f727008bcf0294fc8efdeab75a3", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "0eb5221a586bbcd3f7934af7c77f79e13e19de328900878acec417e5c55a54dd", "schema_version": 1}
|
||||
|
||||
@@ -50,6 +50,7 @@ AOL_LIFECYCLE_TARGETS = (
|
||||
"openpilot/starpilot/aol/tests/test_native_inventory_control_cadence.py",
|
||||
"openpilot/starpilot/aol/tests/test_gm.py",
|
||||
"openpilot/starpilot/aol/tests/test_gm_steering_receive_order.py",
|
||||
"openpilot/starpilot/aol/tests/test_gm_ascm_receive_order.py",
|
||||
"openpilot/starpilot/aol/tests/test_gm_main_response.py",
|
||||
"openpilot/starpilot/aol/tests/test_gm_held_native.py",
|
||||
'openpilot/starpilot/longitudinal/tests/test_volt_mode_transition.py',
|
||||
|
||||
Reference in New Issue
Block a user