mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-13 07:53:45 +08:00
245 lines
13 KiB
Python
245 lines
13 KiB
Python
"""Parsed Volt brake/regen during lag retains intent, never command authority."""
|
|
from contextlib import ExitStack
|
|
from dataclasses import replace
|
|
import os
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
from opendbc.can import CANPacker
|
|
from opendbc.car import Bus
|
|
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
|
|
from opendbc.car.gm.bolt_cc import button_bytes
|
|
from opendbc.car.gm.values import DBC, CruiseButtons, CAR, is_bolt_pedal_profile
|
|
from opendbc.car.gm.tests.test_bolt_pedal import params as bolt_params
|
|
from openpilot.cereal import messaging
|
|
from openpilot.common.params import Params
|
|
from openpilot.common.prefix import OpenpilotPrefix
|
|
from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture
|
|
from openpilot.starpilot.aol.tests import test_gm as gm_fixture
|
|
from openpilot.starpilot.aol.tests.test_gm_startup_main import parsed_packets
|
|
from openpilot.starpilot.aol.tests.test_gm_route18e_recovery import deliver_card
|
|
from openpilot.selfdrive.selfdrived.events import Events, EventName
|
|
from openpilot.starpilot.aol.wire import decode_safety, encode_safety
|
|
|
|
BASE = 10_000_000_000
|
|
|
|
|
|
def callers(cleanup, cp=None):
|
|
params = Params()
|
|
cleanup.callback(params._finalizer)
|
|
params.put_bool('AlwaysOnLateral', True, block=True)
|
|
card = gm_fixture.TestGmAol.card(sdgm_params() if cp is None else cp, params)
|
|
cleanup.callback(card.vehicle_startup.close)
|
|
case = fixture.IoniqAvailabilityTests(methodName='runTest')
|
|
cleanup.callback(case.doCleanups)
|
|
sd, controls, step = case._authority_callers(False, cp=card.CP.as_reader().as_builder(), genuine_events=True)
|
|
card.slc_producer_session = 'authority-card'
|
|
packer = CANPacker(DBC[card.CP.carFingerprint][Bus.pt])
|
|
|
|
def state(tick, pedal='none', main=True):
|
|
now = BASE + tick * 10_000_000
|
|
if is_bolt_pedal_profile(card.CP):
|
|
from openpilot.starpilot.aol.tests.test_gm_held_native import parse
|
|
packets = parse(card, now, gesture={'regen': 'paddle'}.get(pedal, pedal), counter=tick % 4, packets_only=True)
|
|
packets = [packet for packet in packets if packet[0] != 0xC9]
|
|
packets.append(packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {'ACCCruiseState': 0}))
|
|
packets.append(packer.make_can_msg('ECMEngineStatus', 0, {'CruiseMainOn': int(main), 'BrakePressed': int(pedal == 'brake')}))
|
|
if tick == 0:
|
|
card.CI.update([(now - 1_000_000, packets)])
|
|
cs = card.CI.update([(now, packets)])
|
|
assert cs.canValid and not cs.canTimeout
|
|
return cs
|
|
packets = parsed_packets(card.CP, packer, now, tick, 20., main)
|
|
if card.CP.pcmCruise:
|
|
# Factory adaptive standby; state zero reports non-adaptive cruise.
|
|
address = packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {})[0]
|
|
packets = [row for row in packets if not (row[0] == address and row[2] == 2)]
|
|
packets.append(packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {'ACCCruiseState': 2}))
|
|
addresses = {packer.make_can_msg(name, 0, {})[0] for name in ('ECMAcceleratorPos', 'EBCMRegenPaddle')}
|
|
packets = [p for p in packets if p[0] not in addresses]
|
|
packets += [packer.make_can_msg('ECMAcceleratorPos', 0, {'BrakePedalPos': 12 if pedal == 'brake' else 0}),
|
|
packer.make_can_msg('EBCMRegenPaddle', 0, {'RegenPaddle': int(pedal == 'regen')})]
|
|
if pedal == 'cancel':
|
|
packets = [p for p in packets if p[0] != 0x1E1]
|
|
packets.append((0x1E1, button_bytes(CruiseButtons.CANCEL, tick % 4), 0))
|
|
cs = card.CI.update([(now, packets)])
|
|
assert cs.canValid and not cs.canTimeout
|
|
assert cs.brakePressed == (pedal == 'brake') and cs.regenBraking == (pedal == 'regen')
|
|
return cs
|
|
|
|
def drive(tick, cs, **kwargs):
|
|
source = messaging.new_message('carState', valid=True, logMonoTime=BASE + tick * 10_000_000)
|
|
source.carState = cs
|
|
return step(tick, car_state_message=source, latched=card.aol_card_intent.allowed_latch,
|
|
native_request_long=False, **kwargs)
|
|
|
|
cs = state(0)
|
|
card.aol_card_intent.update(cs, now_ns=BASE)
|
|
assert card.aol_card_intent.allowed_latch
|
|
for tick in (0, 1):
|
|
cs = state(tick)
|
|
axis, _, command = drive(tick, cs)
|
|
assert axis.lateralActive and command.latActive and not command.longActive
|
|
deliver_card(card, sd, controls, cs, tick)
|
|
assert card.aol_card_intent.allowed_latch
|
|
return card, sd, controls, state, drive
|
|
|
|
|
|
@pytest.mark.parametrize('profile', [None, *[(candidate, removed) 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)]])
|
|
@pytest.mark.parametrize('pedal', ['brake', 'regen'])
|
|
@pytest.mark.parametrize('order', ['together', 'axis_first', 'event_first'])
|
|
def test_parsed_pedal_lag_keeps_intent_without_renewing_authority(pedal, order, profile):
|
|
with OpenpilotPrefix(), ExitStack() as cleanup:
|
|
cp = None if profile is None else bolt_params(profile[0], pedal=True, camera=not profile[1], removed=profile[1])
|
|
card, sd, controls, parse, drive = callers(cleanup, cp)
|
|
feedback = card.aol_transport_feedback
|
|
baseline = (feedback.lateral_baseline_ns, feedback.lateral_source_ns, feedback.lateral_native_ns,
|
|
feedback.healthy_feedback_ns, feedback.healthy_native_ns)
|
|
cs = parse(2, pedal)
|
|
with mock.patch.object(sd.rk.avg_dt, 'get_average', return_value=.02):
|
|
axis, _, command = drive(2, cs)
|
|
assert not command.latActive and not command.longActive and str(axis.faultReason) == 'availabilityPause'
|
|
assert not axis.lateralActive and not axis.longitudinalActive and not axis.nativeAcknowledged
|
|
assert axis.faultSessionId == sd.aol_session_id and axis.faultEventMonoTime == BASE + 20_000_000
|
|
assert sd.aol_published_fault_proof is None
|
|
assert not axis.desiredLateral and not axis.desiredLongitudinal
|
|
proof, event = sd.pm.messages['aolAxisState'], sd.pm.messages['onroadEvents']
|
|
if order != 'together':
|
|
# Deliver one new envelope first, retaining the previously received peer.
|
|
key, held = ('onroadEvents', event) if order == 'axis_first' else ('aolAxisState', proof)
|
|
sd.pm.messages.pop(key)
|
|
old = card.sm[key]
|
|
replacement = messaging.new_message(key, len(old) if key == 'onroadEvents' else None,
|
|
valid=True, logMonoTime=card.sm.logMonoTime[key])
|
|
setattr(replacement, key, old)
|
|
sd.pm.messages[key] = replacement.as_reader()
|
|
deliver_card(card, sd, controls, cs, 2)
|
|
assert card.aol_card_intent.allowed_latch
|
|
sd.pm.messages[key] = held
|
|
deliver_card(card, sd, controls, cs, 2)
|
|
assert card.aol_card_intent.allowed_latch and not feedback.failed
|
|
assert baseline == (feedback.lateral_baseline_ns, feedback.lateral_source_ns, feedback.lateral_native_ns,
|
|
feedback.healthy_feedback_ns, feedback.healthy_native_ns)
|
|
cs = parse(3)
|
|
axis, _, command = drive(3, cs, allowed=False, native_request_lat=False)
|
|
assert not axis.lateralActive and not command.latActive and not command.longActive
|
|
assert axis.desiredLateral and str(axis.faultReason) == 'none'
|
|
deliver_card(card, sd, controls, cs, 3)
|
|
assert card.aol_card_intent.allowed_latch and not feedback.failed
|
|
cs = parse(4)
|
|
axis, _, command = drive(4, cs)
|
|
assert axis.lateralActive and command.latActive and not command.longActive
|
|
# A real MAIN-off packet still clears the retained intent.
|
|
cs = parse(5, main=False)
|
|
drive(5, cs)
|
|
deliver_card(card, sd, controls, cs, 5)
|
|
assert not card.aol_card_intent.allowed_latch
|
|
|
|
|
|
@pytest.mark.parametrize('bad', ['source_zero', 'source_stale', 'producer', 'epoch', 'session', 'config',
|
|
'commIssue', 'controlsMismatch', 'processNotRunning', 'buttonCancel', 'native_long', 'host_long',
|
|
'cancel', 'main_off', 'settings_off', 'can_invalid', 'permanent_eps', 'door', 'belt', 'no_pedal', 'low_only'])
|
|
def test_pedal_overlap_never_bypasses_other_rejection(bad):
|
|
with OpenpilotPrefix(), ExitStack() as cleanup:
|
|
card, sd, controls, parse, drive = callers(cleanup)
|
|
cs = parse(2, 'brake')
|
|
with mock.patch.object(sd.rk.avg_dt, 'get_average', return_value=.02):
|
|
_, _, command = drive(2, cs)
|
|
assert not command.latActive and not command.longActive
|
|
proof = sd.pm.messages['aolAxisState'].as_builder()
|
|
if bad == 'source_zero':
|
|
proof.aolAxisState.sourceCarStateMonoTime = 0
|
|
elif bad == 'source_stale':
|
|
proof.aolAxisState.sourceCarStateMonoTime -= 30_000_001
|
|
elif bad == 'producer':
|
|
proof.aolAxisState.intentProducerSessionId = 'other'
|
|
elif bad == 'epoch':
|
|
proof.aolAxisState.intentIntegrityEpoch += 1
|
|
elif bad == 'session':
|
|
proof.aolAxisState.sessionId = 'other'
|
|
elif bad == 'config':
|
|
card.CP.safetyConfigs[0].safetyParam ^= 0x400
|
|
elif bad in ('commIssue', 'controlsMismatch', 'processNotRunning', 'buttonCancel'):
|
|
events = Events(sd.events.event_rules)
|
|
events.add_from_msg(sd.pm.messages['onroadEvents'].onroadEvents)
|
|
events.add(getattr(EventName, bad))
|
|
message = sd.pm.messages['onroadEvents'].as_builder()
|
|
message.onroadEvents = events.to_msg()
|
|
sd.pm.messages['onroadEvents'] = message.as_reader()
|
|
elif bad == 'host_long':
|
|
proof.aolAxisState.desiredLongitudinal = True
|
|
elif bad == 'native_long':
|
|
wire = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=BASE + 20_000_000)
|
|
state = decode_safety(bytes(sd.sm['aolSafetyWire']))
|
|
wire.aolSafetyWire = encode_safety(replace(state, requestedLongitudinal=True, longitudinalAllowed=True))
|
|
sd.sm.update_msgs((BASE + 20_000_000) / 1e9, [wire.as_reader()])
|
|
elif bad in ('cancel', 'main_off', 'no_pedal', 'low_only'):
|
|
cs = parse(2, 'cancel' if bad == 'cancel' else 'none', main=bad != 'main_off')
|
|
if bad == 'low_only':
|
|
from opendbc.car.structs import car
|
|
cs.gearShifter = car.CarState.GearShifter.low
|
|
elif bad == 'settings_off':
|
|
card.aol_card_intent.settings = replace(card.aol_card_intent.settings, enabled=False)
|
|
else:
|
|
setattr(cs, {'can_invalid': 'canValid', 'permanent_eps': 'steerFaultPermanent',
|
|
'door': 'doorOpen', 'belt': 'seatbeltUnlatched'}[bad], bad != 'can_invalid')
|
|
sd.pm.messages['aolAxisState'] = proof.as_reader()
|
|
baseline = (card.aol_transport_feedback.lateral_baseline_ns, card.aol_transport_feedback.lateral_source_ns,
|
|
card.aol_transport_feedback.lateral_native_ns)
|
|
deliver_card(card, sd, controls, cs, 2)
|
|
intent_only = bad in ('commIssue', 'processNotRunning', 'native_long', 'belt')
|
|
assert card.aol_card_intent.allowed_latch == intent_only
|
|
if intent_only:
|
|
# Qualified SOFT withdrawal retains intent, never an output permission.
|
|
assert str(proof.aolAxisState.faultReason) == 'availabilityPause'
|
|
assert not proof.aolAxisState.lateralActive and not proof.aolAxisState.longitudinalActive
|
|
assert not proof.aolAxisState.nativeAcknowledged
|
|
assert not card.sm['carControl'].latActive and not card.sm['carControl'].longActive
|
|
assert baseline == (card.aol_transport_feedback.lateral_baseline_ns, card.aol_transport_feedback.lateral_source_ns,
|
|
card.aol_transport_feedback.lateral_native_ns)
|
|
|
|
|
|
@pytest.mark.parametrize('pedal', ['brake', 'regen'])
|
|
def test_later_physical_cancel_after_retained_lag(pedal):
|
|
with OpenpilotPrefix(), ExitStack() as cleanup:
|
|
card, sd, controls, parse, drive = callers(cleanup)
|
|
cs = parse(2, pedal)
|
|
with mock.patch.object(sd.rk.avg_dt, 'get_average', return_value=.02):
|
|
drive(2, cs)
|
|
deliver_card(card, sd, controls, cs, 2)
|
|
assert card.aol_card_intent.allowed_latch
|
|
cs = parse(3, 'cancel')
|
|
drive(3, cs)
|
|
deliver_card(card, sd, controls, cs, 3)
|
|
# Cancel remains governed by the existing GM mapping and withdraws LONG.
|
|
assert not controls.pm.messages['carControl'].carControl.longActive
|
|
|
|
|
|
@pytest.mark.parametrize('pause_speed', [0., 100.])
|
|
def test_configured_brake_pause_still_inhibits_output(pause_speed):
|
|
from openpilot.starpilot.aol.intent import read_settings
|
|
with OpenpilotPrefix(), ExitStack() as cleanup:
|
|
card, sd, _, parse, drive = callers(cleanup)
|
|
settings = replace(read_settings(sd.params), pause_brake_mps=pause_speed)
|
|
sd.aol_settings = card.aol_card_intent.settings = settings
|
|
axis, _, command = drive(2, parse(2, 'brake'))
|
|
assert axis.lateralActive == command.latActive == (pause_speed == 0.)
|
|
assert not axis.longitudinalActive and not command.longActive
|
|
|
|
|
|
@pytest.mark.parametrize('simulation', [None, '1'])
|
|
def test_fixture_restores_outer_prefix_and_simulation(monkeypatch, simulation):
|
|
if simulation is None:
|
|
monkeypatch.delenv('SIMULATION', raising=False)
|
|
else:
|
|
monkeypatch.setenv('SIMULATION', simulation)
|
|
with OpenpilotPrefix():
|
|
outer = os.environ.get('OPENPILOT_PREFIX')
|
|
with OpenpilotPrefix(), ExitStack() as cleanup:
|
|
cleanup.enter_context(mock.patch.dict(os.environ, {'SIMULATION': '0'}))
|
|
callers(cleanup)
|
|
assert os.environ.get('OPENPILOT_PREFIX') == outer
|
|
assert os.environ.get('SIMULATION') == simulation
|