mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
963 lines
50 KiB
Python
963 lines
50 KiB
Python
"""Real GM parser/Card sources with controlled native peers; no transport claim."""
|
|
from dataclasses import replace
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from opendbc.can import CANPacker
|
|
from opendbc.car import Bus
|
|
from opendbc.car.gm.bolt_cc import button_bytes
|
|
from opendbc.car.gm.gmcan import pedal_crc
|
|
from opendbc.car.gm.tests.test_bolt_cc import feed
|
|
from opendbc.car.gm.tests.test_bolt_pedal import params
|
|
from opendbc.car.gm.values import CAR, DBC, CruiseButtons
|
|
from openpilot.cereal import messaging
|
|
from openpilot.common.params import Params
|
|
from openpilot.common.prefix import OpenpilotPrefix
|
|
from openpilot.starpilot.aol.runtime import decide_axes
|
|
from openpilot.starpilot.aol.transport_pause import TransportPauseFeedback
|
|
from openpilot.starpilot.aol.wire import PandaSlot, SafetyState, encode_safety
|
|
|
|
NOW = 10_000_000_000
|
|
|
|
|
|
class Peers:
|
|
def __init__(self, messages):
|
|
self.messages = {m.which(): m for m in messages}
|
|
self.logMonoTime = {n: int(m.logMonoTime) for n, m in self.messages.items()}
|
|
self.seen = dict.fromkeys(self.messages, True)
|
|
self.valid = dict(self.seen)
|
|
self.alive = dict(self.seen)
|
|
|
|
def __getitem__(self, name):
|
|
return getattr(self.messages[name], name)
|
|
|
|
|
|
def parse(card, now, gesture=None, counter=0, *, belt_unlatched=False, packets_only=False):
|
|
packer = CANPacker(DBC[card.CP.carFingerprint][Bus.pt])
|
|
_, packets = feed(SimpleNamespace(update=lambda _: None), packer, now, counter=counter,
|
|
active=False, speed=20., brake=gesture == 'brake', regen=gesture == 'paddle')
|
|
packets = [p for p in packets if p[0] not in (0x1E1, 0x1F5)]
|
|
packets += [packer.make_can_msg('BCMBlindSpotMonitor', 0, {}),
|
|
packer.make_can_msg('ECMPRDNL2', 0, {'PRNDL2': 6}),
|
|
(0x1E1, button_bytes(CruiseButtons.CANCEL if gesture == 'cancel' else CruiseButtons.UNPRESS, counter), 0)]
|
|
if belt_unlatched:
|
|
# Real physical BCMDoorBeltStatus (0x12A), not a CarState assignment.
|
|
packets = [p for p in packets if p[0] != 0x12A]
|
|
packets.append(packer.make_can_msg('BCMDoorBeltStatus', 0, {'LeftSeatBelt': 0}))
|
|
sensor = bytearray(packer.make_can_msg('GAS_SENSOR', 0,
|
|
{'INTERCEPTOR_GAS': 0., 'INTERCEPTOR_GAS2': 0., 'STATE': 0, 'COUNTER_PEDAL': counter})[1])
|
|
sensor[5] = pedal_crc(sensor)
|
|
packets.append((0x201, bytes(sensor), 0))
|
|
if card.CP.carFingerprint == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL and int(card.CP.safetyConfigs[0].safetyParam) == 0x1CD:
|
|
if now % 40_000_000 < 10_000_000:
|
|
packets.append(packer.make_can_msg('EBCMFrictionBrakeCmd', 2, {'RollingCounter': (now // 40_000_000) % 4}))
|
|
if packets_only:
|
|
return packets
|
|
if now == NOW:
|
|
card.CI.update([(now - 1_000_000, packets)])
|
|
cs = card.CI.update([(now, packets)])
|
|
assert cs.canValid and not cs.canTimeout
|
|
assert cs.brakePressed == (gesture == 'brake')
|
|
assert cs.regenBraking == (gesture == 'paddle')
|
|
assert cs.seatbeltUnlatched == belt_unlatched
|
|
card._aol_can_observation = (cs, now, now, now)
|
|
return cs
|
|
|
|
|
|
def peers(cp, producer, now, *, controls=True, native=None, sequence=1, long_active=True):
|
|
config = cp.safetyConfigs[0]
|
|
if native is None:
|
|
slot = PandaSlot(0, 'gm-unit', config.safetyModel.raw, config.safetyParam, 32, True)
|
|
native = SafetyState(1, True, now, now + 200_000_000, config.safetyModel.raw, config.safetyParam,
|
|
True, True, True, True, 'gm-unit', 'gm-session',
|
|
sourcePandaStatesMonoTime=now, pandaInventory=(slot,))
|
|
wire = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=native.observedMonoTime)
|
|
wire.aolSafetyWire = encode_safety(native)
|
|
pandas = messaging.new_message('pandaStates', 1, valid=True, logMonoTime=now)
|
|
p = pandas.pandaStates[0]
|
|
p.safetyModel, p.safetyParam, p.alternativeExperience = config.safetyModel, config.safetyParam, 32
|
|
p.controlsAllowed = controls
|
|
axis = messaging.new_message('aolAxisState', valid=True, logMonoTime=now)
|
|
a = axis.aolAxisState
|
|
a.sessionId = a.faultSessionId = 'gm-session'
|
|
a.sequence, a.qualified, a.faultReason = sequence, True, 'none'
|
|
a.observedMonoTime, a.validUntilMonoTime, a.sourceCarStateMonoTime = now, now + 30_000_000, now
|
|
a.desiredLateral = a.lateralActive = a.nativeAcknowledged = True
|
|
a.desiredLongitudinal = a.longitudinalActive = long_active
|
|
a.intentProducerSessionId, a.intentIntegrityEpoch = producer, 1
|
|
events = messaging.new_message('onroadEvents', 0, valid=True, logMonoTime=now)
|
|
return Peers([wire.as_reader(), pandas.as_reader(), axis.as_reader(), events.as_reader()]), native
|
|
|
|
|
|
def baseline():
|
|
from openpilot.starpilot.aol.tests.test_gm import TestGmAol
|
|
settings = Params()
|
|
settings.put_bool('AlwaysOnLateral', True, block=True)
|
|
settings.put_bool('AlphaLongitudinalEnabled', True, block=True)
|
|
cp = params(CAR.CHEVROLET_BOLT_CC_2018_2021, pedal=True, camera=True)
|
|
card = TestGmAol.card(cp, settings)
|
|
cs = parse(card, NOW)
|
|
card.aol_card_intent.update(cs, fault_active=False, now_ns=NOW)
|
|
assert card.aol_card_intent.allowed_latch
|
|
sm, native = peers(card.CP, card.slc_producer_session, NOW)
|
|
feedback = TransportPauseFeedback(card.CP)
|
|
assert not feedback.observe(sm, card.CP, cs, NOW, latched=True,
|
|
intent_producer_session=card.slc_producer_session)
|
|
assert feedback.session == 'gm-session' and feedback.healthy_inventory
|
|
return card, feedback, native
|
|
|
|
|
|
@pytest.mark.parametrize('health_allowed', [False, True])
|
|
def test_native_long_withdrawal_before_parser_keeps_existing_card_intent(health_allowed):
|
|
with OpenpilotPrefix():
|
|
card, feedback, prior = baseline()
|
|
now = NOW + 10_000_000
|
|
cs = parse(card, now, counter=1)
|
|
native = replace(prior, observedMonoTime=now, validUntilMonoTime=now + 200_000_000,
|
|
longitudinalAllowed=False, pandaInventory=(replace(prior.pandaInventory[0], controlsAllowed=health_allowed),))
|
|
sm, _ = peers(card.CP, card.slc_producer_session, now, controls=health_allowed, native=native, sequence=2)
|
|
# The host's last full-axis result precedes the native paddle/brake edge.
|
|
reset = feedback.observe(sm, card.CP, cs, now, latched=True,
|
|
intent_producer_session=card.slc_producer_session)
|
|
card.aol_card_intent.update(cs, fault_active=reset, now_ns=now)
|
|
assert not reset and not feedback.failed and card.aol_card_intent.allowed_latch
|
|
assert not native.longitudinalAllowed
|
|
|
|
|
|
@pytest.mark.parametrize('gesture', ['brake', 'paddle', 'cancel'])
|
|
def test_real_physical_withdrawal_retains_latch_with_held_long_wire(gesture):
|
|
with OpenpilotPrefix():
|
|
card, feedback, native = baseline()
|
|
now = NOW + 10_000_000
|
|
cs = parse(card, now, gesture, counter=1)
|
|
source = card.aol_gm_withdrawal_source(cs, now)
|
|
assert source == now
|
|
sm, _ = peers(card.CP, card.slc_producer_session, now, controls=False, native=native, sequence=2)
|
|
reset = feedback.observe(sm, card.CP, cs, now, latched=True, gm_withdrawal_source_ns=source,
|
|
intent_producer_session=card.slc_producer_session)
|
|
card.aol_card_intent.update(cs, fault_active=reset, now_ns=now)
|
|
assert not reset and not feedback.failed and card.aol_card_intent.allowed_latch
|
|
assert feedback.gm_withdrawal_ns == source
|
|
# The prior LONG wire and latest raw withdrawal are untouched. Ordinary
|
|
# runtime keeps LONG off even though accepted driver LAT intent survives.
|
|
intent = SimpleNamespace(allowedLatch=True, pauseLateral=False, pauseLongitudinal=False)
|
|
decision = 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=5.)
|
|
assert decision.lateral_active and not decision.longitudinal_active
|
|
assert native.requestedLongitudinal and native.longitudinalAllowed
|
|
assert native.validUntilMonoTime == NOW + 200_000_000
|
|
# Held and release samples retain one original occurrence; the actual
|
|
# native LAT-only acknowledgement ends the exception on the normal path.
|
|
for offset, physical in ((20_000_000, gesture), (30_000_000, None)):
|
|
later = NOW + offset
|
|
cs = parse(card, later, physical, counter=2)
|
|
sm, _ = peers(card.CP, card.slc_producer_session, later, controls=False, native=native,
|
|
sequence=offset // 10_000_000 + 1, long_active=False)
|
|
assert not feedback.observe(sm, card.CP, cs, later, latched=True,
|
|
gm_withdrawal_source_ns=card.aol_gm_withdrawal_source(cs, later), intent_producer_session=card.slc_producer_session)
|
|
assert feedback.gm_withdrawal_ns == source
|
|
lat_only = replace(native, observedMonoTime=NOW + 40_000_000, validUntilMonoTime=NOW + 240_000_000,
|
|
requestedLongitudinal=False, longitudinalAllowed=False,
|
|
pandaInventory=(replace(native.pandaInventory[0], controlsAllowed=False),))
|
|
sm, _ = peers(card.CP, card.slc_producer_session, NOW + 40_000_000, controls=False,
|
|
native=lat_only, sequence=5, long_active=False)
|
|
assert not feedback.observe(sm, card.CP, cs, NOW + 40_000_000, latched=True,
|
|
intent_producer_session=card.slc_producer_session)
|
|
assert feedback.gm_withdrawal_ns == 0
|
|
|
|
|
|
@pytest.mark.parametrize('fault', ['no_source', 'stale_source', 'future_source', 'producer', 'epoch',
|
|
'session', 'serial', 'inventory', 'owner', 'config', 'rx_fault', 'heartbeat', 'lat_denied', 'no_lat',
|
|
'native_expired', 'can_invalid', 'eps', 'event', 'axis_expired', 'sequence', 'main',
|
|
'seatbelt', 'seatbelt_event', 'door', 'gear'])
|
|
def test_physical_withdrawal_keeps_identity_freshness_and_health_vetoes(fault):
|
|
with OpenpilotPrefix():
|
|
card, feedback, native = baseline()
|
|
now = NOW + 10_000_000
|
|
cs = parse(card, now, 'brake', counter=1)
|
|
source = card.aol_gm_withdrawal_source(cs, now)
|
|
if fault == 'no_source':
|
|
source = 0
|
|
elif fault == 'stale_source':
|
|
source = now - 30_000_001
|
|
elif fault == 'future_source':
|
|
source = now + 1
|
|
elif fault == 'session':
|
|
native = replace(native, axisSessionId='other')
|
|
elif fault == 'serial':
|
|
native = replace(native, pandaSerial='other')
|
|
elif fault == 'inventory':
|
|
native = replace(native, pandaInventory=(replace(native.pandaInventory[0], hardwareSerial='other'),))
|
|
elif fault == 'owner':
|
|
card.CP.alternativeExperience = 0
|
|
elif fault == 'no_lat':
|
|
native = replace(native, requestedLateral=False)
|
|
elif fault == 'native_expired':
|
|
native = replace(native, validUntilMonoTime=now - 1)
|
|
elif fault == 'main':
|
|
cs.cruiseState.available = False
|
|
elif fault == 'lat_denied':
|
|
native = replace(native, lateralAllowed=False)
|
|
elif fault == 'can_invalid':
|
|
cs.canValid = False
|
|
elif fault == 'eps':
|
|
cs.steerFaultPermanent = True
|
|
elif fault == 'seatbelt':
|
|
cs.seatbeltUnlatched = True
|
|
elif fault == 'door':
|
|
cs.doorOpen = True
|
|
elif fault == 'gear':
|
|
from opendbc.car.structs import car
|
|
cs.gearShifter = car.CarState.GearShifter.park
|
|
sm, _ = peers(card.CP, card.slc_producer_session, now, controls=False, native=native, sequence=2)
|
|
axis = sm.messages['aolAxisState'].as_builder()
|
|
pandas = sm.messages['pandaStates'].as_builder()
|
|
events = sm.messages['onroadEvents'].as_builder()
|
|
if fault == 'producer':
|
|
axis.aolAxisState.intentProducerSessionId = 'other'
|
|
elif fault == 'epoch':
|
|
axis.aolAxisState.intentIntegrityEpoch = 2
|
|
elif fault == 'sequence':
|
|
axis.aolAxisState.sequence = 1
|
|
elif fault == 'axis_expired':
|
|
axis.aolAxisState.validUntilMonoTime = now - 1
|
|
elif fault == 'config':
|
|
pandas.pandaStates[0].safetyParam ^= 1
|
|
elif fault == 'rx_fault':
|
|
pandas.pandaStates[0].safetyRxChecksInvalid = True
|
|
elif fault == 'heartbeat':
|
|
pandas.pandaStates[0].heartbeatLost = True
|
|
elif fault == 'seatbelt_event':
|
|
events.init('onroadEvents', 1)
|
|
events.onroadEvents[0].name, events.onroadEvents[0].softDisable = 'seatbeltNotLatched', True
|
|
elif fault == 'event':
|
|
events.init('onroadEvents', 1)
|
|
events.onroadEvents[0].name, events.onroadEvents[0].immediateDisable = 'controlsMismatch', True
|
|
sm = Peers([sm.messages['aolSafetyWire'], pandas.as_reader(), axis.as_reader(), events.as_reader()])
|
|
reset = feedback.observe(sm, card.CP, cs, now, latched=True, gm_withdrawal_source_ns=source,
|
|
intent_producer_session=card.slc_producer_session)
|
|
if fault == 'seatbelt':
|
|
# Keep the original input, but correct its demonstrated contract: an
|
|
# unlatched belt locks LONG and does not erase accepted LAT intent.
|
|
card.aol_card_intent.update(cs, fault_active=reset, now_ns=now)
|
|
assert not reset and not feedback.failed and card.aol_card_intent.allowed_latch
|
|
intent = SimpleNamespace(allowedLatch=True, pauseLateral=False, pauseLongitudinal=False)
|
|
decision = decide_axes(standard_lateral=False, standard_longitudinal=True, 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.)
|
|
assert decision.lateral_active
|
|
assert not decision.desired_longitudinal and not decision.longitudinal_active
|
|
else:
|
|
assert reset
|
|
assert feedback.failed
|
|
|
|
|
|
def test_held_and_duplicate_samples_cannot_extend_withdrawal_deadline():
|
|
with OpenpilotPrefix():
|
|
card, feedback, native = baseline()
|
|
occurrence = NOW + 10_000_000
|
|
for tick in range(1, 23):
|
|
now = NOW + tick * 10_000_000
|
|
cs = parse(card, now, 'brake', counter=tick % 4)
|
|
if tick > 1:
|
|
native = replace(native, observedMonoTime=now, validUntilMonoTime=now + 200_000_000)
|
|
sm, _ = peers(card.CP, card.slc_producer_session, now, controls=False, native=native,
|
|
sequence=tick + 1, long_active=tick == 1)
|
|
source = card.aol_gm_withdrawal_source(cs, now)
|
|
for _ in range(2):
|
|
failed = feedback.observe(sm, card.CP, cs, now, latched=True, gm_withdrawal_source_ns=source,
|
|
intent_producer_session=card.slc_producer_session)
|
|
assert failed == (tick == 22)
|
|
assert feedback.gm_withdrawal_ns == occurrence
|
|
assert feedback.failed
|
|
|
|
|
|
@pytest.mark.parametrize('fault', ['different_cs', 'old_receipt', 'future_receipt', 'stale_parser', 'future_parser'])
|
|
def test_physical_receipt_requires_same_parsed_batch_and_separate_clock_freshness(fault):
|
|
with OpenpilotPrefix():
|
|
card, _, _ = baseline()
|
|
now = NOW + 10_000_000
|
|
cs = parse(card, now, 'brake', counter=1)
|
|
assert card.aol_gm_withdrawal_source(cs, now) == now
|
|
if fault == 'different_cs':
|
|
card._aol_can_observation = (cs.as_reader().as_builder(), now, now, now)
|
|
elif fault == 'old_receipt':
|
|
card._aol_can_observation = (cs, now - 30_000_001, now, now)
|
|
elif fault == 'future_receipt':
|
|
card._aol_can_observation = (cs, now + 1, now, now)
|
|
elif fault == 'stale_parser':
|
|
card._aol_can_observation = (cs, now, now + 30_000_001, now + 30_000_001)
|
|
elif fault == 'future_parser':
|
|
card._aol_can_observation = (cs, now, now - 1, now)
|
|
assert card.aol_gm_withdrawal_source(cs, now) == 0
|
|
|
|
|
|
@pytest.mark.parametrize('owner', ['qualified', 'unmarked', 'wrong_profile'])
|
|
def test_actual_card_state_update_captures_boot_clock_only_for_physical_gm_owner(owner):
|
|
# Stop immediately after the production clock gate. The real joined Card.step
|
|
# transport cases separately exercise installation/consumption of the receipt.
|
|
class ClockBoundaryReached(Exception):
|
|
pass
|
|
|
|
def stop_after_clock(*_):
|
|
raise ClockBoundaryReached
|
|
|
|
with OpenpilotPrefix():
|
|
card, _, _ = baseline()
|
|
cs = parse(card, NOW + 10_000_000, 'brake', counter=1)
|
|
if owner == 'unmarked':
|
|
card.CP.alternativeExperience = 0
|
|
elif owner == 'wrong_profile':
|
|
card.CP.flags = 0
|
|
with patch('openpilot.selfdrive.car.card.messaging.drain_sock_raw', return_value=[]), \
|
|
patch('openpilot.selfdrive.car.card.can_capnp_to_list', return_value=[(NOW + 10_000_000, [])]), \
|
|
patch('openpilot.selfdrive.car.card.time.monotonic_ns', return_value=NOW + 10_000_000), \
|
|
patch('openpilot.selfdrive.car.card.time.clock_gettime_ns', return_value=NOW + 10_000_000) as clock, \
|
|
patch.object(card.CI, 'update', return_value=cs), \
|
|
patch.object(card, 'observe_ioniq6_long_authority', side_effect=stop_after_clock):
|
|
with pytest.raises(ClockBoundaryReached):
|
|
card.state_update()
|
|
if owner == 'qualified':
|
|
from openpilot.selfdrive.car.card import time
|
|
clock.assert_called_once_with(time.CLOCK_BOOTTIME)
|
|
else:
|
|
clock.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize('fault', [None, 'unmarked', 'wrong_owner', 'unlatched', 'unarmed', 'source_mismatch',
|
|
'source_expired', 'future_source', 'no_cancel', 'can_invalid', 'eps', 'no_native', 'lat_denied', 'native_inventory'])
|
|
def test_gm_cancel_exempts_only_current_armed_lateral_no_entry(fault):
|
|
from openpilot.starpilot.aol.wire import IntentState
|
|
from openpilot.starpilot.car.gm.aol import physical_cancel_lateral_allowed
|
|
from openpilot.selfdrive.selfdrived.events import ET, EVENTS, EventName
|
|
from openpilot.starpilot.nostalgia import aol_no_entry
|
|
with OpenpilotPrefix():
|
|
card, _, native = baseline()
|
|
now = NOW + 10_000_000
|
|
cs = parse(card, now, 'cancel', counter=1)
|
|
intent = IntentState(card.slc_producer_session, 2, now, now, now + 30_000_000, True, False, False, True, True)
|
|
source = now
|
|
if fault == 'unmarked':
|
|
card.CP.alternativeExperience = 0
|
|
elif fault == 'wrong_owner':
|
|
card.CP.flags = 0
|
|
elif fault == 'unlatched':
|
|
intent = replace(intent, allowedLatch=False)
|
|
elif fault == 'unarmed':
|
|
intent = replace(intent, lateralArmed=False)
|
|
elif fault == 'source_mismatch':
|
|
source -= 1
|
|
elif fault == 'source_expired':
|
|
now += 30_000_001
|
|
elif fault == 'future_source':
|
|
source += 1
|
|
elif fault == 'no_cancel':
|
|
cs.buttonEvents = []
|
|
elif fault == 'can_invalid':
|
|
cs.canValid = False
|
|
elif fault == 'eps':
|
|
cs.steerFaultPermanent = True
|
|
elif fault == 'no_native':
|
|
native = None
|
|
elif fault == 'lat_denied':
|
|
native = replace(native, lateralAllowed=False)
|
|
elif fault == 'native_inventory':
|
|
native = replace(native, pandaInventory=(replace(native.pandaInventory[0], hardwareSerial='other'),))
|
|
allowed = physical_cancel_lateral_allowed(card.CP, cs, intent, native, source_ns=source, now_ns=now)
|
|
assert allowed == (fault is None)
|
|
assert aol_no_entry([EventName.buttonCancel], cs, paddle_only_cancel=False)
|
|
assert aol_no_entry([EventName.buttonCancel], cs, paddle_only_cancel=False, allow_lateral_cancel=allowed) == (fault is not None)
|
|
assert ET.USER_DISABLE in EVENTS[EventName.buttonCancel]
|
|
assert aol_no_entry([EventName.buttonCancel, EventName.doorOpen], cs,
|
|
paddle_only_cancel=False, allow_lateral_cancel=allowed)
|
|
|
|
|
|
def test_native_lat_only_after_long_belt_loss_keeps_saved_lateral_intent():
|
|
from openpilot.starpilot.aol.intent import disarming_fault
|
|
from openpilot.starpilot.nostalgia import aol_no_entry
|
|
from openpilot.selfdrive.selfdrived.events import Events, EventName
|
|
with OpenpilotPrefix():
|
|
card, feedback, native = baseline()
|
|
for tick, unlatched in ((1, True), (2, True), (3, False)):
|
|
now = NOW + tick * 10_000_000
|
|
cs = parse(card, now, counter=tick)
|
|
cs.seatbeltUnlatched = unlatched
|
|
native = replace(native, observedMonoTime=now, validUntilMonoTime=now + 200_000_000,
|
|
requestedLongitudinal=False, longitudinalAllowed=False,
|
|
pandaInventory=(replace(native.pandaInventory[0], controlsAllowed=False),))
|
|
sm, _ = peers(card.CP, card.slc_producer_session, now, controls=False, native=native,
|
|
sequence=tick + 1, long_active=False)
|
|
events = Events()
|
|
if unlatched:
|
|
events.add(EventName.seatbeltNotLatched)
|
|
wire_events = sm.messages['onroadEvents'].as_builder()
|
|
wire_events.onroadEvents = events.to_msg()
|
|
sm = Peers([sm.messages['aolSafetyWire'], sm.messages['pandaStates'],
|
|
sm.messages['aolAxisState'], wire_events.as_reader()])
|
|
reset = feedback.observe(sm, card.CP, cs, now, latched=True,
|
|
intent_producer_session=card.slc_producer_session)
|
|
card.aol_card_intent.update(cs, fault_active=reset or disarming_fault(events.to_msg(), cs), now_ns=now)
|
|
assert not reset and not feedback.failed and card.aol_card_intent.allowed_latch
|
|
allowed, pause_lat, pause_long = card.aol_card_intent.output(cs)
|
|
decision = decide_axes(standard_lateral=True, standard_longitudinal=unlatched,
|
|
intent=SimpleNamespace(allowedLatch=allowed, pauseLateral=pause_lat, pauseLongitudinal=pause_long),
|
|
native=native, car_state=cs, initialized=True, model_ready=True,
|
|
no_entry=aol_no_entry(events.names, cs, paddle_only_cancel=False),
|
|
immediate_disable=False, dm_lockout=False, pause_brake_mps=0.)
|
|
assert decision.lateral_active
|
|
assert not decision.desired_longitudinal and not decision.longitudinal_active
|
|
assert feedback.gm_withdrawal_ns == 0
|
|
|
|
|
|
@pytest.mark.parametrize('fault', [None, 'stale_source', 'future_source', 'future_observation',
|
|
'excessive_lease', 'expired', 'no_expiry'])
|
|
def test_real_card_published_cancel_intent_preserves_producer_lease_and_freshness(fault):
|
|
from openpilot.starpilot.aol.runtime import current_intent
|
|
from openpilot.starpilot.aol.wire import decode_intent
|
|
from openpilot.starpilot.car.gm.aol import physical_cancel_lateral_allowed
|
|
|
|
class PublishedCarState(Exception):
|
|
pass
|
|
|
|
with OpenpilotPrefix():
|
|
card, _, native = baseline()
|
|
now = NOW + 10_000_000
|
|
cs = parse(card, now, 'cancel', counter=1)
|
|
published = {}
|
|
real_new_message = messaging.new_message
|
|
|
|
def stamped_message(service=None, size=None, **kwargs):
|
|
return real_new_message(service, size, **({'logMonoTime': now} | kwargs))
|
|
|
|
def capture(service, message):
|
|
wire = message if isinstance(message, bytes) else message.to_bytes()
|
|
published[service] = messaging.log_from_bytes(wire)
|
|
if service == 'carState':
|
|
raise PublishedCarState
|
|
|
|
with patch('openpilot.selfdrive.car.card.messaging.new_message', side_effect=stamped_message), \
|
|
patch.object(card.pm, 'send', side_effect=capture):
|
|
with pytest.raises(PublishedCarState):
|
|
card.state_publish(cs, None)
|
|
source = int(published['carState'].logMonoTime)
|
|
cs = published['carState'].carState
|
|
intent = decode_intent(published['aolIntentWire'].aolIntentWire)
|
|
assert intent is not None
|
|
assert intent.carStateLogMonoTime == intent.observedMonoTime == source == now
|
|
assert intent.validUntilMonoTime - intent.observedMonoTime == 200_000_000
|
|
admitted = current_intent(Peers([published['aolIntentWire']]), car_state_ns=source, now_ns=now)
|
|
assert admitted == intent
|
|
# Faults modify only the negative cases after the actual producer boundary.
|
|
if fault == 'stale_source':
|
|
now += 30_000_001
|
|
elif fault == 'future_source':
|
|
source += 1
|
|
elif fault == 'future_observation':
|
|
intent = replace(intent, observedMonoTime=now + 1)
|
|
elif fault == 'excessive_lease':
|
|
intent = replace(intent, validUntilMonoTime=intent.observedMonoTime + 200_000_001)
|
|
elif fault == 'expired':
|
|
intent = replace(intent, validUntilMonoTime=now - 1)
|
|
elif fault == 'no_expiry':
|
|
intent = replace(intent, validUntilMonoTime=0)
|
|
assert physical_cancel_lateral_allowed(card.CP, cs, intent, native,
|
|
source_ns=source, now_ns=now) == (fault is None)
|
|
|
|
|
|
@pytest.mark.parametrize('gesture', ['brake', 'paddle', 'cancel'])
|
|
def test_real_belt_loss_with_physical_withdrawal_retains_only_existing_lateral_intent(gesture):
|
|
from openpilot.starpilot.aol.intent import disarming_fault
|
|
from openpilot.starpilot.nostalgia import aol_no_entry
|
|
from openpilot.selfdrive.selfdrived.events import Events, EventName
|
|
with OpenpilotPrefix():
|
|
card, feedback, native = baseline()
|
|
occurrence = NOW + 10_000_000
|
|
for tick, unlatched, physical in ((1, True, gesture), (2, True, gesture),
|
|
(3, True, None), (4, False, None)):
|
|
now = NOW + tick * 10_000_000
|
|
cs = parse(card, now, physical, counter=tick % 4, belt_unlatched=unlatched)
|
|
sm, _ = peers(card.CP, card.slc_producer_session, now, controls=False, native=native,
|
|
sequence=tick + 1, long_active=tick == 1)
|
|
events = Events()
|
|
# At the physical boundary Card consumes the prior event publication;
|
|
# the following call consumes the real seatbelt soft-disable classification.
|
|
if unlatched and tick > 1:
|
|
events.add(EventName.seatbeltNotLatched)
|
|
event_wire = sm.messages['onroadEvents'].as_builder()
|
|
event_wire.onroadEvents = events.to_msg()
|
|
sm = Peers([sm.messages['aolSafetyWire'], sm.messages['pandaStates'],
|
|
sm.messages['aolAxisState'], event_wire.as_reader()])
|
|
source = card.aol_gm_withdrawal_source(cs, now)
|
|
assert source == (now if physical is not None and (gesture != 'cancel' or tick == 1) else 0)
|
|
reset = feedback.observe(sm, card.CP, cs, now, latched=True,
|
|
gm_withdrawal_source_ns=source, intent_producer_session=card.slc_producer_session)
|
|
card.aol_card_intent.update(cs, fault_active=reset or disarming_fault(events.to_msg(), cs), now_ns=now)
|
|
assert not reset and not feedback.failed and card.aol_card_intent.allowed_latch
|
|
assert feedback.gm_withdrawal_ns == occurrence
|
|
assert native.requestedLongitudinal and native.longitudinalAllowed
|
|
assert native.observedMonoTime == NOW and native.validUntilMonoTime == NOW + 200_000_000
|
|
allowed, pause_lat, pause_long = card.aol_card_intent.output(cs)
|
|
intent = SimpleNamespace(allowedLatch=allowed, pauseLateral=pause_lat, pauseLongitudinal=pause_long)
|
|
no_entry = aol_no_entry(events.names, cs, paddle_only_cancel=False)
|
|
decision = decide_axes(standard_lateral=True, standard_longitudinal=unlatched,
|
|
intent=intent, native=native, car_state=cs, initialized=True, model_ready=True,
|
|
no_entry=no_entry, immediate_disable=False, dm_lockout=False, pause_brake_mps=0.)
|
|
assert decision.desired_lateral and decision.lateral_active
|
|
# Belt loss locks LONG even with a standard LONG request. Rebuckling
|
|
# supplies no new standard engagement and cannot resume LONG automatically.
|
|
assert not decision.desired_longitudinal and not decision.longitudinal_active
|
|
locked = decide_axes(standard_lateral=True, standard_longitudinal=unlatched,
|
|
intent=intent, native=native, car_state=cs, initialized=True, model_ready=True,
|
|
no_entry=no_entry, immediate_disable=False, dm_lockout=True, pause_brake_mps=0.)
|
|
assert not locked.lateral_active and not locked.longitudinal_active
|
|
ack_ns = NOW + 50_000_000
|
|
cs = parse(card, ack_ns, counter=1)
|
|
lat_only = replace(native, observedMonoTime=ack_ns, validUntilMonoTime=ack_ns + 200_000_000,
|
|
requestedLongitudinal=False, longitudinalAllowed=False,
|
|
pandaInventory=(replace(native.pandaInventory[0], controlsAllowed=False),))
|
|
sm, _ = peers(card.CP, card.slc_producer_session, ack_ns, controls=False,
|
|
native=lat_only, sequence=6, long_active=False)
|
|
assert not feedback.observe(sm, card.CP, cs, ack_ns, latched=True,
|
|
intent_producer_session=card.slc_producer_session)
|
|
assert not feedback.failed and feedback.gm_withdrawal_ns == 0
|
|
|
|
|
|
@pytest.mark.parametrize('fault', ['belt_immediate', 'belt_user_disable', 'belt_missing_noentry',
|
|
'wrong_belt_name', 'other_soft', 'mixed_soft', 'stale_events', 'future_events', 'door', 'eps'])
|
|
def test_combined_belt_withdrawal_preserves_exact_event_and_vehicle_fault_vetoes(fault):
|
|
from openpilot.selfdrive.selfdrived.events import Events, EventName
|
|
with OpenpilotPrefix():
|
|
card, feedback, native = baseline()
|
|
now = NOW + 10_000_000
|
|
cs = parse(card, now, 'brake', counter=1, belt_unlatched=True)
|
|
source = card.aol_gm_withdrawal_source(cs, now)
|
|
assert source == now
|
|
sm, _ = peers(card.CP, card.slc_producer_session, now, controls=False, native=native, sequence=2)
|
|
events = Events()
|
|
events.add(EventName.seatbeltNotLatched)
|
|
if fault in ('other_soft', 'mixed_soft'):
|
|
if fault == 'other_soft':
|
|
events.clear()
|
|
events.add(EventName.processNotRunning)
|
|
event_wire = sm.messages['onroadEvents'].as_builder()
|
|
event_wire.onroadEvents = events.to_msg()
|
|
if fault == 'belt_immediate':
|
|
event_wire.onroadEvents[0].immediateDisable = True
|
|
elif fault == 'belt_user_disable':
|
|
event_wire.onroadEvents[0].userDisable = True
|
|
elif fault == 'belt_missing_noentry':
|
|
event_wire.onroadEvents[0].noEntry = False
|
|
elif fault == 'wrong_belt_name':
|
|
event_wire.onroadEvents[0].name = EventName.steerTempUnavailable
|
|
elif fault == 'stale_events':
|
|
event_wire.logMonoTime = now - 1_500_000_001
|
|
elif fault == 'future_events':
|
|
event_wire.logMonoTime = now + 1
|
|
elif fault == 'door':
|
|
cs.doorOpen = True
|
|
elif fault == 'eps':
|
|
cs.steerFaultPermanent = True
|
|
sm = Peers([sm.messages['aolSafetyWire'], sm.messages['pandaStates'],
|
|
sm.messages['aolAxisState'], event_wire.as_reader()])
|
|
assert feedback.observe(sm, card.CP, cs, now, latched=True,
|
|
gm_withdrawal_source_ns=source, intent_producer_session=card.slc_producer_session)
|
|
assert feedback.failed
|
|
|
|
|
|
@pytest.mark.parametrize('gesture', ['brake', 'paddle', 'cancel'])
|
|
@pytest.mark.parametrize('fault', [None, 'other_soft', 'immediate'])
|
|
def test_actual_sd_belt_classification_wire_reaches_next_card_cycle(gesture, fault):
|
|
from contextlib import ExitStack
|
|
|
|
from openpilot.selfdrive.selfdrived.events import Events, EventName
|
|
from openpilot.starpilot.aol.intent_companion import IntentCompanion
|
|
from openpilot.starpilot.aol.tests.test_ioniq_availability import IoniqAvailabilityTests
|
|
from openpilot.starpilot.aol.transport_pause import AvailabilityPause, TransportPause
|
|
from openpilot.starpilot.aol.wire import decode_intent
|
|
|
|
class PublishedCarState(Exception):
|
|
pass
|
|
|
|
case = IoniqAvailabilityTests(methodName='runTest')
|
|
with ExitStack() as cleanup, OpenpilotPrefix():
|
|
cleanup.callback(case.doCleanups)
|
|
card, _, native = baseline()
|
|
sock, queued = object(), []
|
|
companion = IntentCompanion(sock)
|
|
sd, _, step = case._authority_callers(True, cp=card.CP.as_reader().as_builder(), intent_companion=companion)
|
|
sd.aol_session_id = 'gm-session'
|
|
sd.aol_transport_pause = TransportPause(sd.CP, sd.aol_session_id)
|
|
sd.aol_availability_pause = AvailabilityPause(sd.CP, sd.aol_session_id)
|
|
original_receive = messaging.recv_one_or_none
|
|
cleanup.enter_context(patch('openpilot.starpilot.aol.intent_companion.messaging.recv_one_or_none',
|
|
side_effect=lambda target: queued.pop(0) if target is sock and queued else None if target is sock else original_receive(target)))
|
|
current = {'now': NOW, 'controls': True, 'events': Events()}
|
|
original_update = sd.sm.update_msgs
|
|
|
|
def receive_native(timestamp, messages):
|
|
controlled, _ = peers(card.CP, card.slc_producer_session, current['now'],
|
|
controls=current['controls'], native=native)
|
|
originals = [m for m in messages if m.which() not in ('aolSafetyWire', 'pandaStates')]
|
|
original_update(timestamp, originals + [controlled.messages[n] for n in ('aolSafetyWire', 'pandaStates')])
|
|
|
|
cleanup.enter_context(patch.object(sd.sm, 'update_msgs', side_effect=receive_native))
|
|
def receive_events(cs):
|
|
sd.events = current['events']
|
|
sd._update_steering_authority(cs)
|
|
|
|
cleanup.enter_context(patch.object(sd, 'update_events', side_effect=receive_events))
|
|
real_new_message = messaging.new_message
|
|
|
|
def publish_card(cs, now):
|
|
published = {}
|
|
|
|
def capture(service, message):
|
|
published[service] = messaging.log_from_bytes(message if isinstance(message, bytes) else message.to_bytes())
|
|
if service == 'carState':
|
|
raise PublishedCarState
|
|
|
|
with patch('openpilot.selfdrive.car.card.messaging.new_message',
|
|
side_effect=lambda service=None, size=None, **kwargs: real_new_message(service, size, **({'logMonoTime': now} | kwargs))), \
|
|
patch.object(card.pm, 'send', side_effect=capture):
|
|
with pytest.raises(PublishedCarState):
|
|
card.state_publish(cs, None)
|
|
assert decode_intent(published['aolIntentWire'].aolIntentWire).validUntilMonoTime == now + 200_000_000
|
|
queued.append(published['aolIntentWire'])
|
|
return published['aolIntentWire'].as_builder()
|
|
|
|
# First establish the baseline from actual SD.step and its real publisher.
|
|
healthy = parse(card, NOW)
|
|
initial = publish_card(healthy, NOW)
|
|
axis, _, _ = step(0, observed_state=healthy, intent_message=initial)
|
|
assert str(axis.faultReason) == 'none' and axis.lateralActive and axis.longitudinalActive
|
|
card.aol_transport_feedback = TransportPauseFeedback(card.CP)
|
|
|
|
def receive_card(now, physical, unlatched, counter):
|
|
controlled, _ = peers(card.CP, card.slc_producer_session, now, controls=current['controls'], native=native)
|
|
frames = [controlled.messages[n] for n in ('aolSafetyWire', 'pandaStates')]
|
|
frames += [sd.pm.messages[n] for n in ('aolAxisState', 'onroadEvents')]
|
|
card.sm.update_msgs(now / 1e9, frames)
|
|
packets = parse(card, now, physical, counter=counter, belt_unlatched=unlatched, packets_only=True)
|
|
with patch('openpilot.selfdrive.car.card.messaging.drain_sock_raw', return_value=[b'controlled-can-transport']), \
|
|
patch('openpilot.selfdrive.car.card.can_capnp_to_list', return_value=[(now, packets)]), \
|
|
patch('openpilot.selfdrive.car.card.time.monotonic_ns', return_value=now), \
|
|
patch('openpilot.selfdrive.car.card.time.clock_gettime_ns', return_value=now), \
|
|
patch('openpilot.selfdrive.car.card.REPLAY', False), patch.object(card.sm, 'update'):
|
|
cs, _ = card.state_update()
|
|
assert cs.canValid and cs.seatbeltUnlatched == unlatched
|
|
return cs
|
|
|
|
receive_card(NOW + 1_000, None, False, 1)
|
|
assert card.aol_transport_feedback.session == 'gm-session' and card.aol_transport_feedback.healthy_inventory
|
|
assert card.aol_transport_feedback.availability_epoch == companion.integrity_epoch
|
|
for tick in (1, 2, 3):
|
|
now = NOW + tick * 10_000_000
|
|
current['now'], current['controls'] = now, False
|
|
cs = receive_card(now, gesture if tick < 3 else None, tick < 3, (tick + 1) % 4)
|
|
if tick == 2 and fault == 'immediate':
|
|
assert card.aol_transport_feedback.failed and not card.aol_card_intent.allowed_latch
|
|
return
|
|
assert not card.aol_transport_feedback.failed and card.aol_card_intent.allowed_latch
|
|
assert card.aol_transport_feedback.gm_withdrawal_ns == NOW + 10_000_000
|
|
events = Events()
|
|
if cs.seatbeltUnlatched:
|
|
events.add(EventName.seatbeltNotLatched)
|
|
if cs.brakePressed or cs.regenBraking:
|
|
events.add(EventName.pedalPressed)
|
|
elif tick == 1 and gesture == 'cancel':
|
|
events.add(EventName.buttonCancel)
|
|
if tick == 1 and fault is not None:
|
|
events.add(EventName.overheat if fault == 'other_soft' else EventName.controlsMismatch)
|
|
current['events'] = events
|
|
produced = publish_card(cs, now)
|
|
axis, _, command = step(tick, observed_state=cs, intent_message=produced)
|
|
assert queued == [] and sd.aol_last_intent.producerSessionId == card.slc_producer_session
|
|
assert axis.intentIntegrityEpoch == companion.integrity_epoch
|
|
assert axis.intentProducerSessionId == card.slc_producer_session
|
|
assert axis.sourceCarStateMonoTime == axis.observedMonoTime == now
|
|
assert axis.validUntilMonoTime == now + 30_000_000
|
|
assert native.observedMonoTime == NOW and native.validUntilMonoTime == NOW + 200_000_000
|
|
assert native.requestedLateral and native.lateralAllowed and native.requestedLongitudinal and native.longitudinalAllowed
|
|
assert not axis.longitudinalActive and not command.longActive
|
|
if fault == 'immediate':
|
|
assert str(axis.faultReason) == 'critical'
|
|
elif fault == 'other_soft' and tick == 1:
|
|
assert str(axis.faultReason) == 'availabilityPause'
|
|
assert not axis.lateralActive and not command.latActive
|
|
else:
|
|
assert str(axis.faultReason) == 'none' and axis.lateralActive
|
|
assert not sd.enabled # Pedal/Cancel still disables ordinary LONG; rebuckling cannot resume it.
|
|
|
|
|
|
@pytest.mark.parametrize('fault', [None, 'owner', 'profile', 'stale_source', 'future_source', 'future_observation',
|
|
'intent_expired', 'excessive_lease', 'unarmed', 'unqualified', 'native_missing', 'native_lateral',
|
|
'native_fault', 'native_session', 'native_expired', 'belt_mismatch', 'event_user', 'event_no_entry'])
|
|
def test_belt_soft_classification_keeps_current_owner_intent_native_and_source_guards(fault):
|
|
from openpilot.selfdrive.selfdrived.events import Events, EventName
|
|
from openpilot.starpilot.aol.wire import IntentState
|
|
from openpilot.starpilot.car.gm.aol import seatbelt_lateral_soft_event_allowed
|
|
|
|
with OpenpilotPrefix():
|
|
card, _, native = baseline()
|
|
now = NOW + 10_000_000
|
|
cs = parse(card, now, 'brake', counter=1, belt_unlatched=True)
|
|
intent = IntentState(card.slc_producer_session, 1, now, now, now + 200_000_000,
|
|
True, False, False, True, True)
|
|
source = now
|
|
events = Events()
|
|
events.add(EventName.seatbeltNotLatched)
|
|
event = events.to_msg()[0]
|
|
if fault == 'owner':
|
|
card.CP.alternativeExperience = 0
|
|
elif fault == 'profile':
|
|
card.CP.carFingerprint = CAR.CHEVROLET_VOLT
|
|
elif fault == 'stale_source':
|
|
now += 30_000_001
|
|
elif fault == 'future_source':
|
|
source += 1
|
|
elif fault == 'future_observation':
|
|
intent = replace(intent, observedMonoTime=now + 1)
|
|
elif fault == 'intent_expired':
|
|
intent = replace(intent, validUntilMonoTime=now - 1)
|
|
elif fault == 'excessive_lease':
|
|
intent = replace(intent, validUntilMonoTime=intent.observedMonoTime + 200_000_001)
|
|
elif fault == 'unarmed':
|
|
intent = replace(intent, allowedLatch=False)
|
|
elif fault == 'unqualified':
|
|
intent = replace(intent, settingsQualified=False)
|
|
elif fault == 'native_missing':
|
|
native = None
|
|
elif fault == 'native_lateral':
|
|
native = replace(native, lateralAllowed=False)
|
|
elif fault == 'native_fault':
|
|
native = replace(native, pandaInventory=(replace(native.pandaInventory[0], faults=1),))
|
|
elif fault == 'native_session':
|
|
native = replace(native, axisSessionId='other')
|
|
elif fault == 'native_expired':
|
|
native = replace(native, validUntilMonoTime=now - 1)
|
|
elif fault == 'belt_mismatch':
|
|
cs.seatbeltUnlatched = False
|
|
elif fault == 'event_user':
|
|
event.userDisable = True
|
|
elif fault == 'event_no_entry':
|
|
event.noEntry = False
|
|
assert seatbelt_lateral_soft_event_allowed(card.CP, cs, intent, native, event,
|
|
source_ns=source, now_ns=now, axis_session_id='gm-session') == (fault is None)
|
|
|
|
|
|
@pytest.mark.parametrize('fault', [None, 'sparse_event', 'no_occurrence', 'same_cycle', 'event_mismatch',
|
|
'publisher_delay', 'future_event', 'axis_stamp', 'fault_session', 'stale_source', 'future_source', 'axis_expired',
|
|
'axis_excessive_lease', 'long_desired', 'long_active', 'event_user', 'event_immediate',
|
|
'event_no_entry', 'mixed_soft', 'producer', 'epoch', 'native_expired', 'occurrence_expired'])
|
|
def test_rebuckled_belt_retention_requires_prior_paired_lat_only_proof(fault):
|
|
from openpilot.selfdrive.selfdrived.events import Events, EventName
|
|
|
|
with OpenpilotPrefix():
|
|
card, feedback, native = baseline()
|
|
occurrence = NOW + 10_000_000
|
|
cs = parse(card, occurrence, 'brake', counter=1, belt_unlatched=True)
|
|
sm, _ = peers(card.CP, card.slc_producer_session, occurrence, controls=False, native=native, sequence=2)
|
|
assert not feedback.observe(sm, card.CP, cs, occurrence, latched=True,
|
|
gm_withdrawal_source_ns=card.aol_gm_withdrawal_source(cs, occurrence),
|
|
intent_producer_session=card.slc_producer_session)
|
|
assert feedback.gm_withdrawal_ns == occurrence
|
|
|
|
now, prior = NOW + 30_000_000, NOW + 20_000_000
|
|
cs = parse(card, now, counter=2)
|
|
assert not cs.seatbeltUnlatched and not cs.brakePressed and not cs.regenBraking
|
|
assert card.aol_gm_withdrawal_source(cs, now) == 0
|
|
sm, _ = peers(card.CP, card.slc_producer_session, prior, controls=False, native=native,
|
|
sequence=3, long_active=False)
|
|
axis = sm.messages['aolAxisState'].as_builder()
|
|
events = sm.messages['onroadEvents'].as_builder()
|
|
ordinary = Events()
|
|
ordinary.add(EventName.seatbeltNotLatched)
|
|
events.onroadEvents = ordinary.to_msg()
|
|
axis.aolAxisState.faultEventMonoTime = events.logMonoTime
|
|
if fault == 'sparse_event':
|
|
events.logMonoTime = axis.aolAxisState.faultEventMonoTime = occurrence
|
|
elif fault == 'no_occurrence':
|
|
feedback.gm_withdrawal_ns = 0
|
|
elif fault == 'same_cycle':
|
|
events.logMonoTime = now
|
|
axis.logMonoTime = now
|
|
axis.aolAxisState.sourceCarStateMonoTime = axis.aolAxisState.observedMonoTime = now
|
|
axis.aolAxisState.validUntilMonoTime = now + 30_000_000
|
|
axis.aolAxisState.faultEventMonoTime = now
|
|
elif fault == 'event_mismatch':
|
|
axis.aolAxisState.faultEventMonoTime -= 1
|
|
elif fault == 'publisher_delay':
|
|
axis.aolAxisState.sourceCarStateMonoTime = events.logMonoTime - 1_000
|
|
elif fault == 'future_event':
|
|
events.logMonoTime = axis.aolAxisState.faultEventMonoTime = prior + 1
|
|
elif fault == 'axis_stamp':
|
|
axis.logMonoTime -= 1
|
|
elif fault == 'fault_session':
|
|
axis.aolAxisState.faultSessionId = 'other-session'
|
|
elif fault == 'stale_source':
|
|
axis.aolAxisState.sourceCarStateMonoTime = now - 30_000_001
|
|
elif fault == 'future_source':
|
|
axis.aolAxisState.sourceCarStateMonoTime = now + 1
|
|
elif fault == 'axis_expired':
|
|
axis.aolAxisState.validUntilMonoTime = now - 1
|
|
elif fault == 'axis_excessive_lease':
|
|
axis.aolAxisState.validUntilMonoTime = prior + 30_000_001
|
|
elif fault == 'long_desired':
|
|
axis.aolAxisState.desiredLongitudinal = True
|
|
elif fault == 'long_active':
|
|
axis.aolAxisState.longitudinalActive = True
|
|
elif fault == 'event_user':
|
|
events.onroadEvents[0].userDisable = True
|
|
elif fault == 'event_immediate':
|
|
events.onroadEvents[0].immediateDisable = True
|
|
elif fault == 'event_no_entry':
|
|
events.onroadEvents[0].noEntry = False
|
|
elif fault == 'mixed_soft':
|
|
ordinary.add(EventName.overheat)
|
|
events.onroadEvents = ordinary.to_msg()
|
|
elif fault == 'producer':
|
|
axis.aolAxisState.intentProducerSessionId = 'other-producer'
|
|
elif fault == 'epoch':
|
|
axis.aolAxisState.intentIntegrityEpoch += 1
|
|
elif fault == 'native_expired':
|
|
native = replace(native, validUntilMonoTime=now - 1)
|
|
sm, _ = peers(card.CP, card.slc_producer_session, prior, controls=False, native=native,
|
|
sequence=3, long_active=False)
|
|
elif fault == 'occurrence_expired':
|
|
now = occurrence + 200_000_001
|
|
prior = now - 10_000_000
|
|
cs = parse(card, now, counter=3)
|
|
axis.logMonoTime = prior
|
|
axis.aolAxisState.sourceCarStateMonoTime = axis.aolAxisState.observedMonoTime = prior
|
|
axis.aolAxisState.validUntilMonoTime = prior + 30_000_000
|
|
events.logMonoTime = axis.aolAxisState.faultEventMonoTime = prior
|
|
native = replace(native, observedMonoTime=prior, validUntilMonoTime=prior + 200_000_000)
|
|
sm, _ = peers(card.CP, card.slc_producer_session, prior, controls=False, native=native,
|
|
sequence=3, long_active=False)
|
|
sm = Peers([sm.messages['aolSafetyWire'], sm.messages['pandaStates'], axis.as_reader(), events.as_reader()])
|
|
reset = feedback.observe(sm, card.CP, cs, now, latched=True,
|
|
gm_withdrawal_source_ns=0, intent_producer_session=card.slc_producer_session)
|
|
card.aol_card_intent.update(cs, fault_active=reset, now_ns=now)
|
|
if fault in (None, 'sparse_event', 'publisher_delay'):
|
|
assert not reset and not feedback.failed and card.aol_card_intent.allowed_latch
|
|
assert feedback.gm_withdrawal_ns == occurrence
|
|
assert native.observedMonoTime == NOW and native.validUntilMonoTime == NOW + 200_000_000
|
|
assert native.requestedLongitudinal and native.longitudinalAllowed
|
|
intent = SimpleNamespace(allowedLatch=True, pauseLateral=False, pauseLongitudinal=False)
|
|
decision = 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=5.)
|
|
assert decision.lateral_active and not decision.desired_longitudinal and not decision.longitudinal_active
|
|
else:
|
|
assert reset and feedback.failed and not card.aol_card_intent.allowed_latch
|
|
|
|
|
|
def _sdgm_pedal_state(card, stamp, tick, gesture=None, manual=False):
|
|
from opendbc.car.gm.tests.test_volt_camera_control import feed_camera
|
|
|
|
packer = CANPacker(DBC[card.CP.carFingerprint][Bus.pt])
|
|
empty = SimpleNamespace(update=lambda _: None)
|
|
_, frames = feed_camera(empty, packer, stamp, counter=tick % 4, speed=16.0, brake=gesture == 'brake', regen=gesture == 'paddle', active=True, eps_status=1)
|
|
# Same recorded forward manual-mode report during Mary paddle withdrawal.
|
|
frames = [f for f in frames if f[0] not in (0x1F5, 0xBE, 0xC9)]
|
|
frames += [
|
|
packer.make_can_msg('ECMAcceleratorPos', 0, {'BrakePedalPos': 8 if gesture == 'brake' else 0}),
|
|
packer.make_can_msg('ECMEngineStatus', 0, {'CruiseMainOn': 1, 'BrakePressed': int(gesture == 'brake')}),
|
|
]
|
|
frames.append(packer.make_can_msg('ECMPRDNL2', 0, {'PRNDL2': 4, 'ManualMode': int(manual)}))
|
|
if tick == 0:
|
|
card.CI.update([(stamp - 1_000_000, frames)])
|
|
cs = card.CI.update([(stamp, frames)])
|
|
assert cs.canValid and not cs.canTimeout
|
|
assert cs.regenBraking == (gesture == 'paddle')
|
|
assert cs.brakePressed == (gesture == 'brake')
|
|
assert str(cs.gearShifter) == ('manumatic' if manual else 'drive')
|
|
return cs
|
|
|
|
|
|
@pytest.mark.parametrize('c9', (False, True))
|
|
@pytest.mark.parametrize('gesture', ('brake', 'paddle'))
|
|
@pytest.mark.parametrize('event_first', (False, True))
|
|
@pytest.mark.parametrize('health_allowed', (False, True))
|
|
def test_sdgm_alpha_pedal_does_not_erase_armed_lateral(c9, gesture, event_first, health_allowed):
|
|
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
|
|
from openpilot.starpilot.aol.tests.test_gm import TestGmAol as GmFixture
|
|
from openpilot.starpilot.aol.wire import IntentState
|
|
|
|
with OpenpilotPrefix():
|
|
saved = Params()
|
|
saved.put_bool('AlwaysOnLateral', True, block=True)
|
|
card = GmFixture.card(sdgm_params(alpha=True, brake_c9=c9), saved)
|
|
try:
|
|
cp = card.CP
|
|
feedback = TransportPauseFeedback(cp)
|
|
cs = _sdgm_pedal_state(card, NOW, 0)
|
|
card.aol_card_intent.update(cs, fault_active=False, now_ns=NOW)
|
|
assert card.aol_card_intent.allowed_latch
|
|
sm, prior = peers(cp, card.slc_producer_session, NOW)
|
|
assert not feedback.observe(sm, cp, cs, NOW, latched=True, intent_producer_session=card.slc_producer_session)
|
|
now = NOW + 10_000_000
|
|
cs = _sdgm_pedal_state(card, now, 1, gesture, True)
|
|
native = replace(
|
|
prior,
|
|
observedMonoTime=now,
|
|
validUntilMonoTime=now + 200_000_000,
|
|
longitudinalAllowed=False,
|
|
pandaInventory=(replace(prior.pandaInventory[0], controlsAllowed=health_allowed),),
|
|
)
|
|
sm, _ = peers(cp, card.slc_producer_session, now, controls=health_allowed, native=native, sequence=2, long_active=False)
|
|
a = sm.messages['aolAxisState'].as_builder()
|
|
a.aolAxisState.faultEventMonoTime = NOW if event_first else now
|
|
# Event first: the last full desired request may precede the ordinary disengagement result.
|
|
a.aolAxisState.desiredLongitudinal = event_first
|
|
e = sm.messages['onroadEvents'].as_builder()
|
|
e.onroadEvents = [{'name': 'pedalPressed', 'userDisable': True, 'noEntry': True}]
|
|
sm = Peers([sm.messages['aolSafetyWire'], sm.messages['pandaStates'], a.as_reader(), e.as_reader()])
|
|
fault = feedback.observe(sm, cp, cs, now, latched=True, intent_producer_session=card.slc_producer_session)
|
|
card.aol_card_intent.update(cs, fault_active=fault, now_ns=now)
|
|
assert not fault and card.aol_card_intent.allowed_latch, (gesture, event_first, health_allowed, feedback.__dict__)
|
|
intent = IntentState(card.slc_producer_session, 2, now, now, now + 200_000_000, True, False, False, True, True)
|
|
d = 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.0,
|
|
)
|
|
assert d.lateral_active and not d.longitudinal_active
|
|
# Fresh independent LAT native acknowledgment after release; LONG stays disabled.
|
|
later = now + 10_000_000
|
|
cs = _sdgm_pedal_state(card, later, 2)
|
|
native = replace(native, observedMonoTime=later, validUntilMonoTime=later + 200_000_000, requestedLongitudinal=False)
|
|
sm, _ = peers(cp, card.slc_producer_session, later, controls=False, native=native, sequence=3, long_active=False)
|
|
assert not feedback.observe(sm, cp, cs, later, latched=True, intent_producer_session=card.slc_producer_session)
|
|
assert card.aol_card_intent.allowed_latch and not native.longitudinalAllowed
|
|
finally:
|
|
card.vehicle_startup.close()
|
|
|
|
|
|
def test_factory_brake_clock_packets_are_scoped_and_follow_25hz_cycles():
|
|
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):
|
|
cp = params(candidate, pedal=True, camera=not removed, removed=removed)
|
|
card = SimpleNamespace(CP=cp)
|
|
for tick in range(9):
|
|
now = NOW + tick * 10_000_000
|
|
packets = parse(card, now, counter=tick % 4, packets_only=True)
|
|
clock = [row for row in packets if row[0] == 0x315]
|
|
expected = candidate == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL and not removed and tick % 4 == 0
|
|
assert len(clock) == int(expected)
|
|
if expected:
|
|
assert clock[0][2] == 2 and len(clock[0][1]) == 5
|
|
assert clock[0][1][4] & 3 == (now // 40_000_000) % 4
|