Files
StarPilot/openpilot/starpilot/aol/tests/test_gm_main_level_contract.py
T
2026-10-23 21:02:48 -05:00

542 lines
29 KiB
Python

"""GM-wide MAIN readiness and Bolt physical withdrawal through current callers.
Native RX earns controls; the extracted current pandad C++ writer requests the
actual host decision. No controls_allowed or native permission flag is forced.
Board heartbeat telemetry is an explicit transport fixture, not an execution of
the firmware heartbeat timer; its reported-loss case must veto retention.
This measures a current caller contract, not an unlogged original blocked call.
"""
from dataclasses import replace
from collections import deque
from contextlib import ExitStack
import os
import json
from pathlib import Path
from unittest import mock
import pytest
from opendbc.can import CANPacker
from opendbc.car import Bus, gen_empty_fingerprint
from opendbc.car.gm.interface import CarInterface
from opendbc.car.gm import gmcan
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
from opendbc.car.gm.values import CruiseButtons, DBC
from opendbc.safety.tests.libsafety import libsafety_py
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.car import card as card_module
from openpilot.selfdrive.selfdrived.events import EventName
from openpilot.starpilot.aol.tests import test_gm as gm_fixture
from openpilot.starpilot.aol.tests import test_ioniq_availability as caller_fixture
from openpilot.starpilot.aol.tests.test_gm_startup_main import parsed_packets
from openpilot.starpilot.aol.wire import PandaSlot, SafetyState, decode_intent, encode_safety
from openpilot.starpilot.car.gm.steering_companion import SteeringCompanion
from openpilot.starpilot.aol.tests.gm_gap_cpp_bridge import gm_host_heartbeat_bridge as gm_host_heartbeat_bridge
BASE = 10_000_000_000
LAST_SOURCE = BASE + 250_000_000
GAP_AGE = 36_415_981
SOURCE_ADVANCE = 73_592_025
NATIVE_BEFORE_SOURCE = 12_576_709
class Capture:
def __init__(self):
self.messages = {}
def send(self, name, event):
raw = event if isinstance(event, bytes) else event.to_bytes()
self.messages[name] = messaging.log_from_bytes(raw)
def joined_case(cleanup, bridge, *, c9=False, pedal='none', engage=False, cp=None, cold_boot=False):
saved = Params()
cleanup.callback(saved._finalizer)
for key in ('OpenpilotEnabledToggle', 'AlwaysOnLateral', 'AlphaLongitudinalEnabled'):
saved.put_bool(key, True, block=True)
cp = sdgm_params(alpha=True, brake_c9=c9) if cp is None else cp
card = gm_fixture.TestGmAol.card(cp, saved)
cleanup.callback(card.vehicle_startup.close)
assert card.CP.alternativeExperience == 32
card.ci_initialized = True # No ECU/device I/O; native permission is earned below.
card.pm = Capture()
case = caller_fixture.IoniqAvailabilityTests(methodName='runTest')
cleanup.callback(case.doCleanups)
producer_message = messaging.new_message # Preserve Card's own publication clock before the SD fixture patches it.
sd, controls, step = case._authority_callers(False, cp=card.CP.as_reader().as_builder(), genuine_events=True)
# Restore the real constructor's settings snapshot removed by this generic fixture.
from openpilot.starpilot.aol.intent import read_settings
sd.aol_settings = read_settings(sd.params)
assert sd.aol_settings.enabled and sd.aol_settings.pause_brake_mps == 0.
cleanup.callback(sd.params._finalizer)
cleanup.callback(controls.params._finalizer)
native = libsafety_py.libsafety
# GM snapshots AE during mode initialization; configure it before selecting hooks.
native.set_alternative_experience(card.CP.alternativeExperience)
assert native.set_safety_hooks(card.CP.safetyConfigs[0].safetyModel.raw,
card.CP.safetyConfigs[0].safetyParam) == 0
# Match the shared native harness: hook selection resets the relay transition
# counter; initialize its established-mode fixture afterwards, then set AE.
native.init_tests()
native.set_alternative_experience(card.CP.alternativeExperience)
assert not native.get_controls_allowed()
bridge.reset_host()
packer = CANPacker(DBC[card.CP.carFingerprint][Bus.pt])
receipt_queue = deque()
sd.steering_companion = SteeringCompanion(sock=object())
original_receive = messaging.recv_one_or_none
original_update = sd.sm.update_msgs
original_controls_update = controls.sm.update_msgs
card_delivered = {}
controls_delivered = {}
delivered = {}
current = {'gap': False, 'raw_ahead': False, 'pedal': 'none', 'warmup_trace': [], 'last_cs': None, 'wire': None, 'pandas': None, 'last_command': None}
def receive(sock):
if sock is sd.steering_companion.sock:
return receipt_queue.popleft() if receipt_queue else None
return original_receive(sock)
def update(now, rows):
if cold_boot and now < (BASE + 180_000_000) / 1e9:
changed = []
for row in rows:
if row.which() == 'deviceMotion':
row = row.as_builder()
row.deviceMotion.inputsOK = False
row = row.as_reader()
changed.append(row)
rows = changed
if current['gap'] and not current['raw_ahead']:
rows = [row for row in rows if row.which() not in ('aolIntentWire', 'carOutput', 'starpilotCarState')]
actual = []
for row in rows:
if row.which() in ('aolSafetyWire', 'pandaStates'):
signature = row.as_builder().to_bytes()
if delivered.get(row.which()) == signature:
continue
delivered[row.which()] = signature
actual.append(row)
return original_update(now, actual)
def controls_update(now, rows):
actual = []
for row in rows:
if current['gap'] and row.which() in ('carState', 'carOutput', 'aolIntentWire', 'starpilotCarState'):
continue
if row.which() in ('aolSafetyWire', 'pandaStates'):
signature = row.as_builder().to_bytes()
if controls_delivered.get(row.which()) == signature:
continue
controls_delivered[row.which()] = signature
actual.append(row)
return original_controls_update(now, actual)
cleanup.enter_context(mock.patch.object(messaging, 'recv_one_or_none', side_effect=receive))
cleanup.enter_context(mock.patch.object(controls.sm, 'update_msgs', side_effect=controls_update))
cleanup.enter_context(mock.patch.object(sd.sm, 'update_msgs', side_effect=update))
def physical(now, tick, *, main=True, button=CruiseButtons.UNPRESS, eps=1):
rows = parsed_packets(card.CP, packer, now, tick, 20., main)
if current['pedal'] != 'none':
from opendbc.car.gm.tests.test_volt_camera_control import feed_camera
from types import SimpleNamespace
_, extra = feed_camera(SimpleNamespace(update=lambda _: None), packer, now,
counter=tick % 4, speed=20., brake=current['pedal'] == 'brake',
regen=current['pedal'] == 'regen', active=False, eps_status=eps)
addresses = {0xBE, 0xC9, 0xF1, 0xBD}
rows = [row for row in rows if row[0] not in addresses]
rows += [row for row in extra if row[0] in addresses and row[0] != 0xBE]
rows.append(packer.make_can_msg('ECMAcceleratorPos', 0,
{'BrakePedalPos': 10 if current['pedal'] == 'brake' else 0}))
if cold_boot and tick < 20:
gear_address = packer.make_can_msg('ECMPRDNL2', 0, {})[0]
wheel_addresses = {packer.make_can_msg(name, 0, {})[0] for name in ('EBCMWheelSpdFront', 'EBCMWheelSpdRear')}
rows = [row for row in rows if not (row[2] == 0 and row[0] in wheel_addresses | {gear_address})]
rows += [packer.make_can_msg('ECMPRDNL2', 0, {'PRNDL2': 2 if tick < 8 else 3 if tick < 14 else 4}),
packer.make_can_msg('EBCMWheelSpdFront', 0, {}),
packer.make_can_msg('EBCMWheelSpdRear', 0, {})]
if card.CP.flags & GMFlags.PEDAL_LONG:
sensor = bytearray(packer.make_can_msg('GAS_SENSOR', 0,
{'INTERCEPTOR_GAS': 0., 'INTERCEPTOR_GAS2': 0., 'STATE': 0, 'COUNTER_PEDAL': tick % 4})[1])
sensor[5] = gmcan.pedal_crc(sensor)
rows.append((0x201, bytes(sensor), 0))
if card.CP.flags & GMFlags.HAS_BSM:
rows.append(packer.make_can_msg('BCMBlindSpotMonitor', 0, {}))
if current['pedal'] != 'none':
# The recorded paddle transition also reports physical manual/regen mode.
# Preserve that actual forward gear; never fabricate a drive-mode CS flag.
gear_address = packer.make_can_msg('ECMPRDNL2', 0, {})[0]
rows = [row for row in rows if not (row[0] == gear_address and row[2] == 0)]
rows.append(packer.make_can_msg('ECMPRDNL2', 0, {'PRNDL2': 6, 'ManualMode': 1}))
rows = [row for row in rows if row[0] not in (0x1E1, 0x184)]
rows += [gmcan.create_buttons(packer, 0, tick % 4, button),
packer.make_can_msg('PSCMStatus', 0, {'LKATorqueDeliveredStatus': eps})]
native.set_timer(now // 1000)
for address, data, bus in rows:
assert native.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data)), (hex(address), bus)
native.safety_tick()
return rows
def sample_native(now, *, heartbeat_lost=False):
axis = sd.pm.messages.get('aolAxisState')
intent = card.pm.messages.get('aolIntentWire')
axis_bytes = axis.as_builder().to_bytes() if axis is not None else b''
intent_bytes = intent.as_builder().to_bytes() if intent is not None else b''
native.set_timer(now // 1000)
plan = bridge.plan_host(4, card.CP.safetyConfigs[0].safetyParam,
native.aol_get_request_mask(), native.aol_get_permission_mask(), now,
axis_bytes, len(axis_bytes), intent_bytes, len(intent_bytes), sd.enabled)
native.aol_set_host_request(plan & 0xFF)
native.set_aol_test_heartbeat(bool(plan & 0x100))
bridge.complete_host(native.aol_get_request_mask(), native.aol_get_permission_mask())
mask, request = native.aol_get_permission_mask(), native.aol_get_request_mask()
bad_rx = not native.safety_config_valid()
relay = bool(native.get_relay_malfunction())
panda = messaging.new_message('pandaStates', 1, valid=True, logMonoTime=now)
row = panda.pandaStates[0]
row.safetyModel, row.safetyParam = card.CP.safetyConfigs[0].safetyModel, card.CP.safetyConfigs[0].safetyParam
row.alternativeExperience, row.controlsAllowed = 32, native.get_controls_allowed()
row.safetyRxChecksInvalid = bad_rx
row.heartbeatLost = heartbeat_lost # Received board-health fixture; safety DLL has no firmware timer.
if relay:
row.faults = ['relayMalfunction']
slot = PandaSlot(0, 'gm-gap-native', 4, row.safetyParam, 32, row.controlsAllowed, bad_rx, heartbeat_lost, int(relay))
wire = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=now)
wire.aolSafetyWire = encode_safety(SafetyState(1, bool(plan & 0x400), now, now + 200_000_000,
4, row.safetyParam, bool(mask & 1), bool(mask & 2), bool(request & 1), bool(request & 2),
'gm-gap-native', sd.aol_session_id, now, (slot,)))
current['wire'], current['pandas'] = wire, panda
return mask
def card_receive(now, rows, *, publish=True):
peers = [current['wire'].as_reader(), current['pandas'].as_reader()]
peers += [sd.pm.messages[name] for name in ('aolAxisState', 'onroadEvents', 'selfdriveState') if name in sd.pm.messages]
if current['last_command'] is not None:
peers.append(current['last_command'])
device = messaging.new_message('deviceState', valid=sd.sm.valid['deviceState'],
logMonoTime=sd.sm.logMonoTime['deviceState'])
device.deviceState = sd.sm['deviceState']
peers.append(device.as_reader())
fresh = []
for row in peers:
signature = row.as_builder().to_bytes()
if card_delivered.get(row.which()) != signature:
card_delivered[row.which()] = signature
fresh.append(row)
card.sm.update_msgs(now / 1e9, fresh)
def card_message(*args, **kwargs):
kwargs.setdefault('logMonoTime', now)
return producer_message(*args, **kwargs)
with mock.patch.object(card_module.time, 'monotonic_ns', return_value=now), \
mock.patch.object(card_module.time, 'monotonic', return_value=now / 1e9), \
mock.patch.object(card_module.time, 'clock_gettime_ns', return_value=now), \
mock.patch.object(card_module.messaging, 'new_message', side_effect=card_message), \
mock.patch.object(card_module.messaging, 'drain_sock_raw', return_value=[b'physical-can']), \
mock.patch.object(card_module, 'can_capnp_to_list', return_value=[(now, rows)]), \
mock.patch.object(card_module, 'REPLAY', False), mock.patch.object(card.sm, 'update'), \
mock.patch.object(card.RI, 'update', return_value=None):
cs, rd = card.state_update()
if publish:
card.state_publish(cs, rd)
current['last_cs'] = cs
return cs
def drive(now, tick, *, gap=False, missing_companion=False):
current['gap'] = gap
assert current['wire'] is not None and current['pandas'] is not None
if (not gap or current['raw_ahead']) and not missing_companion and 'starpilotCarState' in card.pm.messages:
receipt_queue.append(card.pm.messages['starpilotCarState'])
source = card.pm.messages['carState'].as_builder()
intent = card.pm.messages['aolIntentWire'].as_builder()
authority = (card.pm.messages['starpilotCarState'].as_builder() if 'starpilotCarState' in card.pm.messages and
not (gap and not current['raw_ahead']) and not missing_companion else None)
axis, state, command = step(tick, offset_ns=now - (BASE + tick * 10_000_000), timeout=gap,
car_state_message=None if gap else source, intent_message=intent, companion=False,
authority_message=authority, car_output_message=card.pm.messages['carOutput'].as_builder(),
safety_message=current['wire'], panda_state_message=current['pandas'])
current['last_command'] = controls.pm.messages['carControl']
return axis, state, command
def apply_command(now, command):
native.set_timer(now // 1000)
card.pm.messages.pop('sendcan', None)
# Card.step passes the decoded SubMaster reader, never Controls' builder.
# Match the same explicit BOOTTIME/monotonic clock used in card_receive.
received = current['last_command'].carControl
assert received.to_dict() == command.to_dict()
with mock.patch.object(card_module.time, 'monotonic_ns', return_value=now), \
mock.patch.object(card_module.time, 'monotonic', return_value=now / 1e9), \
mock.patch.object(card_module.time, 'clock_gettime_ns', return_value=now):
card.controls_update(current['last_cs'], received)
if 'sendcan' in card.pm.messages:
for frame in card.pm.messages['sendcan'].sendcan:
accepted = native.safety_tx_hook(libsafety_py.make_CANPacket(frame.address, frame.src, bytes(frame.dat)))
if frame.address == 0x3D1 and card.CP.safetyConfigs[0].safetyParam == 157:
# Bolt dashboard bytes are queued for the native physical RX owner.
# Direct USB transmission must be denied, even for a valid replacement.
assert not accepted
else:
assert accepted, (hex(frame.address), frame.src)
native_due = BASE
for tick in range(26):
now = BASE + tick * 10_000_000
if tick == 24:
sample_native(LAST_SOURCE - NATIVE_BEFORE_SOURCE)
native_due = LAST_SOURCE - NATIVE_BEFORE_SOURCE + 100_000_000
rows = physical(now, tick, main=not engage or tick >= 5,
button=CruiseButtons.DECEL_SET if engage and tick == 12 else CruiseButtons.UNPRESS)
if tick == 0:
# Real parser subscriptions need the preceding physical batch, as in
# the existing cold-start parser tests; no validity flag is forced.
card.CI.update([(now - 1_000_000, rows)])
if current['wire'] is None or now >= native_due:
sample_native(now)
native_due = now + 100_000_000
card_receive(now, rows)
axis, state, command = drive(now + 5_467_396, tick)
apply_command(now + 5_467_397, command)
current['warmup_trace'].append((tick, current['last_cs'].cruiseState.available,
card.aol_card_intent.allowed_latch, card.aol_card_intent._main_cycle_required,
card.aol_transport_feedback.failed, str(axis.faultReason), axis.lateralActive,
axis.longitudinalActive, native.aol_get_request_mask(), native.aol_get_permission_mask(),
current['last_cs'].canValid, str(current['last_cs'].gearShifter),
current['last_cs'].steeringDisengage, current['last_cs'].brakePressed,
current['last_cs'].gasPressed, card.pm.messages['aolIntentWire'].aolIntentWire.hex(),
current['wire'].aolSafetyWire.hex()))
if os.environ.get('GM_SDGM_TRACE'):
with Path(os.environ['GM_SDGM_TRACE']).open('a') as out:
out.write(json.dumps({'c9': c9, 'engage': engage, 'warmup': current['warmup_trace']}) + '\n')
assert native.get_controls_allowed() == engage and native.aol_get_permission_mask() == (3 if engage else 1)
assert axis.lateralActive and command.latActive
assert axis.longitudinalActive == engage and command.longActive == engage
assert state.enabled == engage and card.aol_card_intent.allowed_latch, current['warmup_trace']
assert not card.CI.CS.steering_authority.latched
assert decode_intent(card.pm.messages['aolIntentWire'].aolIntentWire).carStateLogMonoTime == LAST_SOURCE
return card, sd, controls, native, current, physical, sample_native, card_receive, drive, apply_command
from opendbc.car.gm.values import CAR, GMFlags
def route_bolt_params():
fingerprint = gen_empty_fingerprint()
fingerprint[0].update({0x201: 6, 0x142: 8})
fingerprint[2].update({0x320: 8, 0x180: 4})
return CarInterface.get_params(CAR.CHEVROLET_BOLT_CC_2018_2021, fingerprint, [], True, False, False)
@pytest.mark.parametrize('pedal', ('brake', 'regen'))
@pytest.mark.parametrize('main_cycle', (False, True))
@pytest.mark.parametrize('initial_engaged', (False, True))
def test_bolt_main_startup_engagement_pedal_and_rearm(gm_host_heartbeat_bridge, pedal, main_cycle, initial_engaged):
with OpenpilotPrefix(), ExitStack() as cleanup, mock.patch.dict(os.environ, {'SIMULATION': '0', 'AOL_REPLAY_RUNTIME': '1'}):
cp = route_bolt_params()
joined = joined_case(cleanup, gm_host_heartbeat_bridge, cp=cp, engage=initial_engaged)
card, sd, controls, native, current, physical, sample_native, card_receive, drive, apply_command = joined
assert card.CP.flags == 17 and card.CP.safetyConfigs[0].safetyParam == 157
# The cold path starts with MAIN already ON and no SET. The second
# path earns ordinary Long first, isolating its pedal return behavior.
# Exercise physical MAIN OFF/ON as a distinct recovery path.
for tick in range(26, 34):
now = BASE + tick * 10_000_000
rows = physical(now, tick, main=not main_cycle or tick >= 30)
sample_native(now)
card_receive(now, rows)
axis, state, command = drive(now + 1, tick)
apply_command(now + 2, command)
if os.environ.get('GM_SDGM_TRACE'):
with Path(os.environ['GM_SDGM_TRACE']).open('a') as out:
out.write(json.dumps({'phase': 'main-cycle', 'tick': tick,
'axis': axis.to_dict(), 'intent': card.pm.messages['aolIntentWire'].aolIntentWire.hex(),
'native': current['wire'].aolSafetyWire.hex(),
'allowed': card.aol_card_intent.allowed_latch,
'mainArm': card.aol_card_intent.main_arm_ns,
'feedbackFailed': card.aol_transport_feedback.failed,
'feedbackSignature': card.aol_transport_feedback.signature,
'mainRequired': card.aol_card_intent._main_cycle_required}) + '\n')
if main_cycle and tick < 30:
assert not axis.lateralActive and not command.latActive
assert not card.aol_card_intent.allowed_latch
assert axis.lateralActive and command.latActive and card.aol_card_intent.allowed_latch
engaged = False
for tick in range(34, 66):
now = BASE + tick * 10_000_000
current['pedal'] = pedal if 46 <= tick < 60 else 'none'
rows = physical(now, tick, button=CruiseButtons.DECEL_SET if tick == 35 else CruiseButtons.UNPRESS)
# Receive native LONG withdrawal before the corresponding host decision,
# as in the route at408.396/408.398s; retain the genuine LAT-only grant.
if tick in (34, 40, 45, 46, 49, 54, 59, 62):
sample_native(now)
card_receive(now, rows)
axis, state, command = drive(now + 1, tick)
if 40 <= tick < 46:
assert state.enabled and axis.longitudinalActive and command.longActive
engaged = True
if tick >= 46:
assert str(current['last_cs'].gearShifter) == ('manumatic' if tick < 60 else 'drive')
assert current['last_cs'].brakePressed == (pedal == 'brake' and tick < 60)
assert current['last_cs'].regenBraking == (pedal == 'regen' and tick < 60)
assert not state.enabled and not command.longActive and not axis.longitudinalActive
assert axis.lateralActive and command.latActive and card.aol_card_intent.allowed_latch
assert not sd.aol_authority_lost and EventName.controlsMismatch not in sd.events.names
apply_command(now + 2, command)
assert engaged
from unittest.mock import patch
from openpilot.selfdrive.car import card as module
from openpilot.starpilot.aol.tests import test_gm as fixtures
from openpilot.starpilot.aol.tests.test_gm_held_native import peers
from openpilot.starpilot.aol.tests.test_gm_startup_main import cases as standard_cold_cases
NOW = BASE
def cold_cases():
return list(standard_cold_cases()) + [route_bolt_params()]
@pytest.mark.parametrize('index', range(9))
@pytest.mark.parametrize('braking', (False, True))
@pytest.mark.parametrize('variation', ('none', 'event_expired', 'source_expired', 'native_expired',
'critical', 'producer', 'epoch', 'session', 'native_request', 'main_off', 'settings_off', 'gas'))
def test_gm_initial_main_waits_on_original_soft_occurrence(index, braking, variation):
with OpenpilotPrefix(), ExitStack() as cleanup, patch.dict(os.environ, {'SIMULATION': '1', 'AOL_REPLAY_RUNTIME': '0'}):
saved = Params()
saved.put_bool('AlwaysOnLateral', True, block=True)
card = fixtures.TestGmAol.card(cold_cases()[index], saved)
cleanup.callback(card.vehicle_startup.close)
packer = CANPacker(DBC[card.CP.carFingerprint][Bus.pt])
card.ci_initialized = True
card.publish_sendcan = lambda *args, **kwargs: None
def packets(now, tick, main=True):
rows = parsed_packets(card.CP, packer, now, tick, 8., main)
if braking:
from opendbc.car.gm.tests.test_volt_camera_control import feed_camera
from types import SimpleNamespace
_, extra = feed_camera(SimpleNamespace(update=lambda _: None), packer, now,
counter=tick % 4, speed=8., brake=True, active=False, eps_status=1)
addresses = {0xBE, 0xC9, 0xF1}
rows = [row for row in rows if row[0] not in addresses]
rows += [row for row in extra if row[0] in addresses and row[0] not in (0xBE, 0xC9)]
rows.append(packer.make_can_msg('ECMAcceleratorPos', 0, {'BrakePedalPos': 10}))
rows.append(packer.make_can_msg('ECMEngineStatus', 0, {'CruiseMainOn': int(main), 'BrakePressed': 1}))
if card.CP.flags & GMFlags.HAS_BSM:
rows.append(packer.make_can_msg('BCMBlindSpotMonitor', 0, {}))
if card.CP.flags & GMFlags.PEDAL_LONG:
sensor = bytearray(packer.make_can_msg('GAS_SENSOR', 0,
{'INTERCEPTOR_GAS': 700. if variation == 'gas' else 0.,
'INTERCEPTOR_GAS2': 700. if variation == 'gas' else 0., 'STATE': 0, 'COUNTER_PEDAL': tick % 4})[1])
sensor[5] = gmcan.pedal_crc(sensor)
rows.append((0x201, bytes(sensor), 0))
if variation == 'gas':
address = packer.make_can_msg('AcceleratorPedal2', 0, {})[0]
rows = [row for row in rows if not (row[0] == address and row[2] == 0)]
rows.append(packer.make_can_msg('AcceleratorPedal2', 0, {'AcceleratorPedal2': 30}))
return rows
for tick in range(20):
stamp = NOW - (20 - tick) * 10_000_000
card.CI.update([(stamp, packets(stamp, tick))])
def cycle(tick, age=None, mutate=False, recovering=False):
now = NOW + tick * 10_000_000
source = now - 1
config = card.CP.safetyConfigs[0]
slot = PandaSlot(0, 'sdgm-idle', config.safetyModel.raw, config.safetyParam, 32, False)
native = SafetyState(1, True, now, now + 200_000_000, config.safetyModel.raw, config.safetyParam,
False, False, False, False, 'sdgm-idle', 'gm-session',
sourcePandaStatesMonoTime=now, pandaInventory=(slot,))
if mutate and variation == 'native_expired':
native = replace(native, observedMonoTime=now - 201_000_000,
validUntilMonoTime=now - 1, sourcePandaStatesMonoTime=now - 201_000_000)
if mutate and variation == 'native_request':
native = replace(native, requestedLateral=True)
incoming, _ = peers(card.CP, card.slc_producer_session, now, controls=False,
native=native, sequence=tick + 1, long_active=False)
axis = incoming.messages['aolAxisState'].as_builder()
a = axis.aolAxisState
a.sourceCarStateMonoTime = now - 31_000_000 if mutate and variation == 'source_expired' else source
a.nativeAcknowledged = age is None
a.desiredLateral = recovering
a.lateralActive = a.desiredLongitudinal = a.longitudinalActive = False
a.faultReason = 'none' if age is None else 'availabilityPause'
event_ns = now if age is None else now - age
if mutate and variation == 'event_expired':
event_ns = now - 1_500_000_001
a.faultEventMonoTime = event_ns
if mutate and variation == 'producer':
a.intentProducerSessionId = 'foreign'
if mutate and variation == 'epoch':
a.intentIntegrityEpoch += 1
if mutate and variation == 'session':
a.sessionId = a.faultSessionId = 'foreign'
events = messaging.new_message('onroadEvents', (1 + int(braking)) if age is not None else 0,
valid=True, logMonoTime=event_ns)
if age is not None:
events.onroadEvents[0].name = 'commIssueAvgFreq'
events.onroadEvents[0].softDisable = True
if braking:
events.onroadEvents[1].name = 'pedalPressed'
events.onroadEvents[1].userDisable = True
if mutate and variation == 'critical':
events.onroadEvents[0].name = 'controlsMismatch'
events.onroadEvents[0].softDisable = False
events.onroadEvents[0].immediateDisable = True
a.faultReason = 'critical'
if mutate and variation == 'settings_off':
saved.put_bool('AlwaysOnLateral', False, block=True)
card.aol_card_intent.settings = module.read_settings(saved)
control = messaging.new_message('carControl', valid=True, logMonoTime=now)
card.sm.update_msgs(now / 1e9, [incoming.messages[k] for k in ('aolSafetyWire', 'pandaStates')] +
[axis.as_reader(), events.as_reader(), control.as_reader()])
rows = packets(now, tick, main=not (mutate and variation == 'main_off'))
batch = messaging.log_from_bytes(module.can_list_to_can_capnp(rows)).as_builder()
batch.logMonoTime = now
with patch.object(module, 'REPLAY', False), \
patch.object(module.messaging, 'drain_sock_raw', return_value=[batch.to_bytes()]), \
patch.object(module.time, 'monotonic_ns', return_value=now), \
patch.object(module.time, 'monotonic', return_value=now / 1e9), \
patch.object(module.time, 'clock_gettime_ns', return_value=now), patch.object(card.sm, 'update'):
card.step()
assert card.CS_prev.canValid and not card.CS_prev.canTimeout
assert card.CS_prev.brakePressed == braking
assert card.CS_prev.gasPressed == (variation == 'gas')
assert not card.last_actuators_output.torqueOutputCan
cycle(0) # MAIN already true; no button event or Long engagement.
assert card.aol_card_intent.allowed_latch
healthy = card.aol_transport_feedback.healthy_feedback_ns
# Hold ONE original event occurrence while real parser/Card samples advance
# at100Hz; independently fresh native/source messages never restamp it.
for tick in range(1, 38):
age = (tick + 3) * 10_000_000
cycle(tick, age, mutate=variation not in ('none', 'gas'))
if variation not in ('none', 'gas'):
assert not card.aol_card_intent.allowed_latch
return
assert card.aol_card_intent.allowed_latch, 'cold MAIN intent was revoked by a current SOFT occurrence'
assert card.aol_transport_feedback.healthy_feedback_ns == healthy
assert not card.aol_transport_feedback.failed
cycle(38) # Healthy idle reply after a cold wait longer than native200ms.
assert card.aol_card_intent.allowed_latch and not card.aol_transport_feedback.failed
cycle(39, recovering=True) # Native OFF while the fresh LAT request awaits acknowledgment.
assert card.aol_card_intent.allowed_latch and not card.aol_transport_feedback.failed
@pytest.mark.parametrize('cp_kind', ('sdgm', 'bolt'))
def test_main_already_on_reverse_neutral_drive_autoarms_without_button_or_long(gm_host_heartbeat_bridge, cp_kind):
with OpenpilotPrefix(), ExitStack() as cleanup, mock.patch.dict(os.environ, {'SIMULATION': '0', 'AOL_REPLAY_RUNTIME': '1'}):
cp = sdgm_params(alpha=True) if cp_kind == 'sdgm' else route_bolt_params()
card, sd, controls, native, current, *_ = joined_case(cleanup, gm_host_heartbeat_bridge, cp=cp, cold_boot=True)
assert not native.get_controls_allowed()
assert card.aol_card_intent.allowed_latch
assert not card.aol_card_intent._main_cycle_required
assert all(row[1] for row in current['warmup_trace'])
assert all(not row[7] for row in current['warmup_trace'])
assert not any(row[6] for row in current['warmup_trace'][:20])
assert any(row[6] for row in current['warmup_trace'][20:])