mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-29 01:23:58 +08:00
Keep GM lateral intent armed through startup and pedal withdrawal
This commit is contained in:
@@ -0,0 +1,508 @@
|
||||
"""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):
|
||||
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 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 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):
|
||||
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 = 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
|
||||
@@ -320,8 +320,8 @@ class TransportPauseFeedback:
|
||||
from opendbc.car import structs
|
||||
ev9 = ev9_qualified(cp, marked_only=True)
|
||||
early_long = gm or early_native_long_withdrawal_supported(cp) or ev9
|
||||
from openpilot.starpilot.car.gm.aol import sdgm_alpha_pedal_withdrawal
|
||||
sdgm_pedal = sdgm_alpha_pedal_withdrawal(cp, cs)
|
||||
from openpilot.starpilot.car.gm.aol import gm_physical_pedal_withdrawal
|
||||
gm_pedal = gm_physical_pedal_withdrawal(cp, cs)
|
||||
event_ns = int(sm.logMonoTime['onroadEvents'])
|
||||
main_off_recovery = bool(0 < event_ns <= self.ccnc_main_off_source_ns <= now_ns and
|
||||
now_ns - self.ccnc_main_off_source_ns <= AXIS_MAX_AGE_NS)
|
||||
@@ -346,7 +346,7 @@ class TransportPauseFeedback:
|
||||
now_ns - int(sm.logMonoTime['onroadEvents']) > 1_500_000_000 or
|
||||
any(event.immediateDisable or event.softDisable or
|
||||
event.userDisable and not (ev9_main_off and event.name == log.OnroadEvent.EventName.wrongCarMode or
|
||||
sdgm_pedal and event.name == log.OnroadEvent.EventName.pedalPressed)
|
||||
gm_pedal and event.name == log.OnroadEvent.EventName.pedalPressed)
|
||||
for event in sm['onroadEvents'])):
|
||||
return None
|
||||
native = independent_lateral_health(sm, cp, cs, self.session, now_ns)
|
||||
@@ -508,8 +508,8 @@ class TransportPauseFeedback:
|
||||
not axis.desiredLongitudinal and not axis.longitudinalActive and
|
||||
all(event.name in (stock_user, log.OnroadEvent.EventName.pedalPressed) and
|
||||
not event.softDisable and not event.immediateDisable for event in users))
|
||||
from openpilot.starpilot.car.gm.aol import sdgm_alpha_pedal_withdrawal
|
||||
pedal = bool((cs.brakePressed or sdgm_alpha_pedal_withdrawal(cp, cs)) and users and all(
|
||||
from openpilot.starpilot.car.gm.aol import gm_physical_pedal_withdrawal
|
||||
pedal = bool((cs.brakePressed or gm_physical_pedal_withdrawal(cp, cs)) and users and all(
|
||||
event.name == log.OnroadEvent.EventName.pedalPressed and
|
||||
not event.softDisable and not event.immediateDisable for event in users) and not faults)
|
||||
if axis.longitudinalActive and not location_fault or not (temporary or eps_recovery or pedal):
|
||||
@@ -1273,10 +1273,10 @@ class TransportPauseFeedback:
|
||||
self.sequence, self.signature = int(axis.sequence), signature
|
||||
return False
|
||||
if cp.brand == 'gm' and current and continuity and latched:
|
||||
from openpilot.starpilot.car.gm.aol import cold_sdgm_main_intent_current
|
||||
from openpilot.starpilot.car.gm.aol import cold_gm_main_intent_current
|
||||
native = (_healthy_transport_native(sm, cp, cs, str(axis.sessionId), now_ns)
|
||||
if cp.brand == 'gm' else None)
|
||||
if cold_sdgm_main_intent_current(self, sm, cp, cs, axis, native, now_ns,
|
||||
if cold_gm_main_intent_current(self, sm, cp, cs, axis, native, now_ns,
|
||||
producer=intent_producer_session):
|
||||
# Event publication has its own unchanged 1.5s freshness contract.
|
||||
# Every axis, source and native lease is validated independently.
|
||||
@@ -1482,7 +1482,14 @@ class TransportPauseFeedback:
|
||||
(native.pandaSerial, tuple(slot.hardwareSerial for slot in native.pandaInventory)) and
|
||||
not native.requestedLateral and not native.lateralAllowed and
|
||||
not native.requestedLongitudinal and not native.longitudinalAllowed)
|
||||
baseline = bool(reason == 'none' and (axis.nativeAcknowledged or inactive) and
|
||||
from openpilot.starpilot.car.gm.aol import main_off_idle_native_baseline
|
||||
gm_main_off = bool(not latched and current and continuity and
|
||||
sm.seen['onroadEvents'] and sm.valid['onroadEvents'] and sm.alive['onroadEvents'] and
|
||||
0 < int(sm.logMonoTime['onroadEvents']) <= now_ns and
|
||||
now_ns - int(sm.logMonoTime['onroadEvents']) <= 1_500_000_000 and
|
||||
axis.faultEventMonoTime == int(sm.logMonoTime['onroadEvents']) and
|
||||
main_off_idle_native_baseline(cp, cs, axis, native, sm['onroadEvents'], producer=intent_producer_session))
|
||||
baseline = bool(reason == 'none' and (axis.nativeAcknowledged or inactive or gm_main_off) and
|
||||
0 < axis.sourceCarStateMonoTime <= stamp and
|
||||
stamp - axis.sourceCarStateMonoTime <= AXIS_MAX_AGE_NS)
|
||||
if native is None and latched and current and continuity and baseline:
|
||||
|
||||
@@ -140,13 +140,13 @@ def sdgm_lateral_pedal_source_zero(feedback, cp, cs, axis, native, now_ns, *, pr
|
||||
not native.requestedLongitudinal and not native.longitudinalAllowed)
|
||||
|
||||
|
||||
def cold_sdgm_main_intent_current(feedback, sm, cp, cs, axis, native, now_ns, *, producer):
|
||||
def cold_gm_main_intent_current(feedback, sm, cp, cs, axis, native, now_ns, *, producer):
|
||||
"""Cold MAIN may wait on a current SOFT occurrence without renewing output."""
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.starpilot.nostalgia import physical_cancel
|
||||
from openpilot.starpilot.aol.runtime import AXIS_MAX_AGE_NS
|
||||
event_ns = int(sm.logMonoTime['onroadEvents'])
|
||||
return bool(sdgm_alpha_lateral_owner(cp) and
|
||||
return bool(qualified_gm(cp) and aol_enabled(cp) and
|
||||
not feedback.failed and not feedback.paused and not feedback.pending_event and
|
||||
not feedback.ever_lateral_active and str(axis.faultReason) == 'availabilityPause' and
|
||||
not axis.desiredLateral and not axis.desiredLongitudinal and
|
||||
@@ -160,8 +160,9 @@ def cold_sdgm_main_intent_current(feedback, sm, cp, cs, axis, native, now_ns, *,
|
||||
sm.valid['onroadEvents'] and sm.alive['onroadEvents'] and
|
||||
0 < event_ns <= axis.observedMonoTime and now_ns - event_ns <= 1_500_000_000 and
|
||||
cs.gearShifter in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low,
|
||||
car.CarState.GearShifter.sport, car.CarState.GearShifter.brake) and
|
||||
not cs.gasPressed and not physical_cancel(cs, cp) and
|
||||
car.CarState.GearShifter.sport, car.CarState.GearShifter.brake,
|
||||
car.CarState.GearShifter.manumatic) and
|
||||
not physical_cancel(cs, cp) and
|
||||
cold_main_readiness(cp, cs, native, sm['onroadEvents']))
|
||||
|
||||
|
||||
@@ -180,6 +181,19 @@ def sdgm_alpha_pedal_withdrawal(cp, cs) -> bool:
|
||||
(cs.brakePressed or cs.regenBraking))
|
||||
|
||||
|
||||
def gm_physical_pedal_withdrawal(cp, cs) -> bool:
|
||||
"""A qualified GM physical pedal withdraws LONG without canceling MAIN intent."""
|
||||
from opendbc.car.structs import car
|
||||
return bool(qualified_gm(cp) and aol_enabled(cp) and
|
||||
cs.canValid and not cs.canTimeout and cs.cruiseState.available and
|
||||
not cs.steerFaultTemporary and not cs.steerFaultPermanent and not cs.accFaulted and
|
||||
not cs.doorOpen and not cs.steeringDisengage and
|
||||
cs.gearShifter in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low,
|
||||
car.CarState.GearShifter.sport, car.CarState.GearShifter.brake,
|
||||
car.CarState.GearShifter.manumatic) and
|
||||
(cs.brakePressed or cs.regenBraking))
|
||||
|
||||
|
||||
def stock_ascm_independent_lateral(cp) -> bool:
|
||||
"""Stock cruise's engagement speed does not cancel independent lateral intent."""
|
||||
return bool(qualified_gm(cp) and cp.carFingerprint == CAR.CHEVROLET_VOLT_ASCM and
|
||||
@@ -314,7 +328,7 @@ def cold_main_readiness(cp, cs, native, events, *, recovering=False):
|
||||
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))
|
||||
return bool(qualified_gm(cp) and aol_enabled(cp) and host_wait and
|
||||
not any(event.userDisable and not (sdgm_alpha_lateral_owner(cp) and
|
||||
not any(event.userDisable and not (gm_physical_pedal_withdrawal(cp, cs) and
|
||||
event.name == log.OnroadEvent.EventName.pedalPressed and (cs.brakePressed or cs.regenBraking) and
|
||||
not event.immediateDisable and not event.softDisable) for event in events) and
|
||||
cs.canValid and not cs.canTimeout and cs.cruiseState.available and
|
||||
@@ -325,6 +339,28 @@ def cold_main_readiness(cp, cs, native, events, *, recovering=False):
|
||||
not native.requestedLongitudinal and not native.longitudinalAllowed)
|
||||
|
||||
|
||||
def main_off_idle_native_baseline(cp, cs, axis, native, events, *, producer):
|
||||
"""Physical MAIN OFF can acknowledge zero native axes despite NO_ENTRY."""
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.cereal import log
|
||||
return bool(qualified_gm(cp) and aol_enabled(cp) and cs.canValid and not cs.canTimeout and
|
||||
not cs.cruiseState.available and not cs.steerFaultTemporary and not cs.steerFaultPermanent and
|
||||
not cs.accFaulted and not cs.doorOpen and not cs.seatbeltUnlatched and not cs.steeringDisengage and
|
||||
cs.gearShifter in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low,
|
||||
car.CarState.GearShifter.sport, car.CarState.GearShifter.brake,
|
||||
car.CarState.GearShifter.manumatic) and
|
||||
producer and str(axis.intentProducerSessionId) == producer and str(axis.faultReason) == 'none' and
|
||||
not axis.desiredLateral and not axis.desiredLongitudinal and
|
||||
not axis.lateralActive and not axis.longitudinalActive and
|
||||
native is not None and native.pandaInventory and
|
||||
not native.requestedLateral and not native.lateralAllowed and
|
||||
not native.requestedLongitudinal and not native.longitudinalAllowed and
|
||||
not any(event.immediateDisable or event.softDisable or event.userDisable and not (
|
||||
event.name in (log.OnroadEvent.EventName.wrongCarMode, log.OnroadEvent.EventName.pcmDisable) or
|
||||
event.name == log.OnroadEvent.EventName.pedalPressed and (cs.brakePressed or cs.regenBraking))
|
||||
for event in events))
|
||||
|
||||
|
||||
def prior_main_off_intent_current(feedback, sm, cp, cs, axis, now_ns, *, latched, main_arm_ns, producer):
|
||||
"""An older idle response cannot reject a newly armed cruise MAIN request."""
|
||||
from opendbc.car.structs import car
|
||||
@@ -345,7 +381,7 @@ def prior_main_off_intent_current(feedback, sm, cp, cs, axis, now_ns, *, latched
|
||||
cs.gearShifter not in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low,
|
||||
car.CarState.GearShifter.sport, car.CarState.GearShifter.brake) or
|
||||
any(event.type == car.CarState.ButtonEvent.Type.cancel for event in cs.buttonEvents) or
|
||||
str(axis.faultReason) != 'none' or axis.nativeAcknowledged and not sdgm_alpha_lateral_owner(cp) or
|
||||
str(axis.faultReason) != 'none' or
|
||||
axis.desiredLateral or axis.desiredLongitudinal or axis.lateralActive or axis.longitudinalActive or
|
||||
not 0 < axis.sourceCarStateMonoTime <= axis.observedMonoTime or axis.sourceCarStateMonoTime >= main_arm_ns or
|
||||
now_ns - axis.sourceCarStateMonoTime > AXIS_MAX_AGE_NS or
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "fa29cc1527cebe8ae361940d0a45abdff00bb3112b7b2341a04a13b79df4b17e", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "b1d278144ba9131ae778d25613afc1a7e0f7a626f321af22dc5181c97e35c118", "schema_version": 1}
|
||||
|
||||
@@ -69,6 +69,7 @@ AOL_LIFECYCLE_TARGETS = (
|
||||
"openpilot/starpilot/aol/tests/test_gm_steering_receive_order.py",
|
||||
"openpilot/starpilot/aol/tests/test_gm_card_source_gap.py",
|
||||
"openpilot/starpilot/aol/tests/test_gm_sdgm_cold_main.py",
|
||||
"openpilot/starpilot/aol/tests/test_gm_main_level_contract.py",
|
||||
"openpilot/starpilot/aol/tests/test_gm_sdgm_source_wait.py",
|
||||
"openpilot/starpilot/aol/tests/test_gm_ascm_receive_order.py",
|
||||
"openpilot/starpilot/aol/tests/test_gm_main_response.py",
|
||||
|
||||
Reference in New Issue
Block a user