Files
StarPilot/openpilot/starpilot/tests/test_bolt_cc_controls_low.py
T

292 lines
16 KiB
Python

import os
import tempfile
import unittest
from unittest.mock import patch
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD
from openpilot.selfdrive.selfdrived.events import EventName
from opendbc.car import structs
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, fixture, native, setup
from opendbc.car.gm.values import CAR
from opendbc.safety.tests.libsafety import libsafety_py
class TestBoltCcControlsLow(unittest.TestCase):
def test_final_params_real_controls_low_drive_override_and_cancel(self):
cases = ((CAR.CHEVROLET_BOLT_CC_2018_2021, False, False),
(CAR.CHEVROLET_BOLT_CC_2018_2021, True, False),
(CAR.CHEVROLET_BOLT_CC_2018_2021, False, True),
(CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL, False, True))
for identity, removed, pedal in cases:
stock_acc = identity == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL
with self.subTest(removed=removed, pedal=pedal), tempfile.TemporaryDirectory() as params_root, \
patch.dict(os.environ, {'PARAMS_ROOT': params_root, 'SIMULATION': '1', 'REPLAY': '1'}), OpenpilotPrefix():
params = Params()
for key in ('OpenpilotEnabledToggle',):
params.put_bool(key, True, block=True)
for key in ('AlwaysOnLateral', 'SafeMode', 'AdvancedLateralTune'):
params.put_bool(key, False, block=True)
cp, ci, packer = fixture(identity, removed=removed, present=pedal)
if stock_acc:
from openpilot.starpilot.aol.tests.test_gm import TestGmAol
selected = TestGmAol.card(cp, params)
cp, ci = selected.CP, selected.CI
self.assertIsNone(selected.aol_card_intent)
self.assertEqual(cp.alternativeExperience, 0)
cp.fingerprintSource = structs.CarParams.FingerprintSource.can
cp.carFw = []
params.put('CarParams', cp.as_reader().as_builder().to_bytes(), block=True)
controls = Controls()
self.assertEqual(controls.CP.to_dict(), cp.to_dict())
self.assertEqual(controls.CP.safetyConfigs[0].safetyParam, 0x1CD if stock_acc else 0x9D if pedal else 0xC121 if removed else 0xC120)
_, template = feed(ci, packer, 900_000_000, active=False, camera=not removed)
setup(cp)
with patch("openpilot.selfdrive.selfdrived.selfdrived.REPLAY", False):
sd = SelfdriveD(CP=cp)
previous_control = structs.CarControl()
active_frames = gas_frames = cancelled_frames = 0
drive_pedal_frames = low_pedal_frames = stock_withdrawals = 0
last_feeds = {}
internal_frames = 0
internal_status_frames = 0
from opendbc.safety.tests.test_gm_bolt_pedal import TestGmBoltPedalSafety
recorder = TestGmBoltPedalSafety()
recorder.safety = libsafety_py.libsafety
if pedal and not stock_acc:
# An arbitrary host status cannot acquire the physical RX-owned feed.
libsafety_py.libsafety.reset_recorded_can()
self.assertFalse(native('tx', (0x3D1, bytes(8), 0), 900_000))
self.assertEqual(recorder.recorded(), [])
for tick in range(360):
now = 1_000_000_000 + tick * 10_000_000
gas = 120 <= tick < 150
button = 3 if 40 <= tick < 42 else 6 if 320 <= tick < 322 else 1
replacement = [
packer.make_can_msg('ECMPRDNL2', 0, {'PRNDL2': 4 if (stock_acc and tick < 180) or 180 <= tick < 220 else 6}),
packer.make_can_msg('ECMEngineStatus', 0, {'CruiseMainOn': int(not stock_acc or tick < 330)}),
packer.make_can_msg('ECMCruiseControl', 0, {'CruiseActive': int(tick >= 42), 'CruiseSetSpeed': 54}),
packer.make_can_msg('AcceleratorPedal2', 0, {'AcceleratorPedal2': 30 if gas else 0, 'CruiseState': int(stock_acc and 240 <= tick < 260)}),
(0x1E1, button_bytes(button, tick % 4), 0),
]
if stock_acc:
if tick % 4 == 0:
replacement.append(packer.make_can_msg('EBCMFrictionBrakeCmd', 2, {'RollingCounter': (tick // 4) % 4}))
replacement.append(packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {'ACCCruiseState': 2}))
replacement.append(packer.make_can_msg('EBCMFrictionBrakeStatus', 0,
{'FrictionBrakeUnavailable': int(310 <= tick < 315)}))
if pedal and tick % 2 == 0:
raw = bytearray.fromhex(('040201fe0000' if gas else '0264011d0000') if stock_acc else
('053502ba0000' if gas else '0279012a0000'))
raw[4] = (tick // 2) % 16
raw[5] = pedal_crc(raw)
replacement.append((0x201, bytes(raw), 0))
replaced = {frame[0] for frame in replacement}
frames = [frame for frame in template if frame[0] not in replaced] + replacement
libsafety_py.libsafety.reset_recorded_can()
for frame in sorted(frames, key=lambda frame: frame[0] in (0x3D1, 0xBD, 0x1F5)):
if frame[0] in (0x184, 0x3D1, 0x1E1, 0xC9, 0x1C4, 0x1F5, 0x34A, 0x201, 0xBD):
self.assertTrue(native('rx', frame, now // 1000))
if pedal:
for address, bus, payload in recorder.recorded():
self.assertEqual(bus, 0)
self.assertEqual(payload, last_feeds[address])
internal_frames += 1
internal_status_frames += int(address == 0x3D1)
libsafety_py.libsafety.safety_tick()
cs = ci.update([(now, frames)])
self.assertTrue(cs.canValid)
expected_gear = (structs.CarState.GearShifter.drive if (stock_acc and tick < 180) or 180 <= tick < 220
else structs.CarState.GearShifter.low)
self.assertEqual(cs.gearShifter, expected_gear)
cs.vCruise = cs.vCruiseCluster = 100.
messages = []
def message(service, size=None, now=now, messages=messages):
msg = messaging.new_message(service, size, valid=True, logMonoTime=now)
messages.append(msg)
return getattr(msg, service)
state = message('carState')
state.from_dict(cs.to_dict())
parameters = message('vehicleParameters')
parameters.stiffnessFactor, parameters.steerRatio = 1., cp.steerRatio
message('longitudinalPlan').aTarget = 1.
message('modelV2').action.desiredCurvature = .00001
message('lateralDelay').lateralDelay = .1
message('carOutput')
peers = []
for service in sd.sm.data:
if service in ('alertDebug', 'lateralManeuverPlan', 'laneChangeAssistWire'):
continue
size = 1 if service == 'pandaStates' else 0 if service == 'laneChangeAssistWire' else None
peer = messaging.new_message(service, size,
valid=True, logMonoTime=now)
payload = getattr(peer, service)
if service == 'pandaStates':
payload[0].safetyModel = cp.safetyConfigs[0].safetyModel
payload[0].safetyParam = cp.safetyConfigs[0].safetyParam
payload[0].alternativeExperience = cp.alternativeExperience
payload[0].controlsAllowed = bool(libsafety_py.libsafety.get_controls_allowed())
payload[0].safetyRxChecksInvalid = not bool(libsafety_py.libsafety.safety_config_valid())
elif service == 'extrinsicsCalibration':
payload.calStatus = 'calibrated'
payload.rpyCalib = [0., 0., 0.]
elif service == 'vehicleParameters':
payload.stiffnessFactor, payload.steerRatio, payload.valid = 1., cp.steerRatio, True
elif service == 'deviceMotion':
payload.inputsOK = payload.posenetOK = True
payload.velocityDevice.x = cs.vEgo
elif service == 'controlsState':
payload.lateralControlState.init('torqueState')
elif service == 'carControl':
payload.from_dict(previous_control.to_dict())
elif service == 'deviceState':
payload.freeSpacePercent = 50.
peers.append(peer.as_reader())
car_state_event = messages[0].as_reader()
with patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', False), \
patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one', return_value=car_state_event), \
patch.object(sd.sm, 'update', side_effect=lambda timeout, now=now, peers=peers, sd=sd:
sd.sm.update_msgs(now / 1e9, peers)):
sd.step()
self.assertEqual(sd.mismatch_counter, int(not stock_acc and tick == 320), tick)
self.assertNotIn(EventName.controlsMismatch, sd.events.names)
host = message('selfdriveState')
host.enabled, host.active, host.state = sd.enabled, sd.active, sd.state_machine.state
event_message = messaging.new_message('onroadEvents', len(sd.events.to_msg()), valid=True, logMonoTime=now)
event_message.onroadEvents = sd.events.to_msg()
messages.append(event_message)
controls.sm.update_msgs(now / 1e9, [msg.as_reader() for msg in messages])
cc, lateral_log = controls.state_control()
self.assertEqual(cc.enabled, sd.enabled)
controls.publish(cc, lateral_log)
self.assertEqual(cc.cruiseControl.cancel, cs.cruiseState.enabled and (not cc.enabled or not cp.pcmCruise))
_, commands = ci.apply(cc.as_reader(), now + 1_000_000)
for frame in commands:
allowed = native('tx', frame, (now + 1_000_000) // 1000)
if pedal and frame[0] in (0xBD, 0x1F5, 0x3D1):
# The host schedules these exact feeds; matching physical RX emits
# them internally. They never grant ordinary host TX permission.
self.assertFalse(allowed)
last_feeds[frame[0]] = frame[1]
else:
self.assertTrue(allowed, (tick, hex(frame[0])))
if stock_acc and (60 <= tick < 100 or 220 <= tick < 240):
pedals = [frame for frame in commands if frame[0] == 0x200]
for frame in pedals:
self.assertTrue(frame[1][4] & 0x80, (tick, cs.gearShifter, frame))
if cs.gearShifter == structs.CarState.GearShifter.drive:
drive_pedal_frames += 1
else:
low_pedal_frames += 1
if stock_acc and 240 <= tick < 260:
for frame in commands:
if frame[0] == 0x200:
self.assertFalse(frame[1][4] & 0x80)
stock_withdrawals += 1
self.assertFalse(any(frame[0] == 0x315 for frame in commands))
self.assertEqual(libsafety_py.libsafety.safety_fwd_hook(2, 0x315), 0)
if stock_acc and tick >= 330:
self.assertFalse(cs.cruiseState.available)
self.assertFalse(any(frame[0] == 0x315 for frame in commands))
self.assertEqual(libsafety_py.libsafety.safety_fwd_hook(2, 0x315), 0)
if stock_acc and 310 <= tick < 315:
self.assertTrue(cs.accFaulted)
self.assertIn(EventName.accFaulted, sd.events.names)
self.assertFalse(cc.enabled or cc.latActive or cc.longActive)
if 45 <= tick < (310 if stock_acc else 320):
self.assertTrue(cc.enabled)
self.assertTrue(cc.latActive)
self.assertTrue(libsafety_py.libsafety.get_controls_allowed())
active_frames += 1
if gas:
self.assertFalse(cc.longActive)
self.assertTrue(cc.cruiseControl.override)
gas_frames += 1
if tick >= 322:
self.assertFalse(cc.enabled or cc.latActive or cc.longActive)
cancelled_frames += 1
previous_control = cc
self.assertGreater(active_frames, 200)
self.assertEqual(gas_frames, 30)
self.assertEqual(cancelled_frames, 38)
if stock_acc:
self.assertGreater(drive_pedal_frames, 0)
self.assertGreater(low_pedal_frames, 0)
self.assertGreater(stock_withdrawals, 0)
if pedal:
if not stock_acc:
self.assertGreater(internal_frames, 0)
self.assertGreater(internal_status_frames, 0)
for index, button in enumerate((3, 1)):
rearm = [frame for frame in frames if frame[0] not in (0x1E1, 0x201, 0xC9)]
rearm.append(packer.make_can_msg("ECMEngineStatus", 0, {"CruiseMainOn": 1}))
raw = bytearray.fromhex('0279012a0000')
raw[4] = (4 + index) % 16
raw[5] = pedal_crc(raw)
rearm += [(0x1E1, button_bytes(button, index), 0), (0x201, bytes(raw), 0)]
for frame in rearm:
if frame[0] in (0x184, 0x1E1, 0xC9, 0x1C4, 0x1F5, 0x34A, 0x201, 0xBD):
self.assertTrue(native('rx', frame, 4_650_000 + index * 20_000))
self.assertTrue(libsafety_py.libsafety.get_controls_allowed())
raw = bytearray.fromhex('0279012a0600')
raw[5] = pedal_crc(raw) ^ 1
self.assertTrue(native('rx', (0x201, bytes(raw), 0), 4_700_000))
self.assertFalse(libsafety_py.libsafety.get_controls_allowed())
ci.update([(4_700_000_000, [(0x201, bytes(raw), 0)])])
self.assertFalse(ci.CS.pedal_sensor_healthy)
continue
maximum_mismatch = 0
mismatch_observed = False
for tick in range(225):
now = 5_000_000_000 + tick * 10_000_000
replacement = [
packer.make_can_msg('ECMPRDNL2', 0, {'PRNDL2': 6}),
packer.make_can_msg('ECMCruiseControl', 0, {'CruiseActive': int(tick < 8), 'CruiseSetSpeed': 54}),
packer.make_can_msg('AcceleratorPedal2', 0, {'AcceleratorPedal2': 0}),
(0x1E1, button_bytes(3 if 2 <= tick < 4 else 1, tick % 4), 0),
]
replaced = {frame[0] for frame in replacement}
frames = [frame for frame in template if frame[0] not in replaced] + replacement
for frame in sorted(frames, key=lambda frame: frame[0] == 0x3D1):
if frame[0] in (0x184, 0x3D1, 0x1E1, 0xC9, 0x1C4, 0x1F5, 0x34A, 0x201, 0xBD):
self.assertTrue(native('rx', frame, now // 1000))
libsafety_py.libsafety.safety_tick()
cs = ci.update([(now, frames)])
self.assertTrue(cs.canValid)
cs.vCruise = cs.vCruiseCluster = 100.
current_peers = []
for peer in peers:
fresh = peer.as_builder()
fresh.logMonoTime = now
if fresh.which() == 'pandaStates':
fresh.pandaStates[0].controlsAllowed = bool(libsafety_py.libsafety.get_controls_allowed())
fresh.pandaStates[0].safetyRxChecksInvalid = not bool(libsafety_py.libsafety.safety_config_valid())
elif fresh.which() == 'carControl':
fresh.carControl.from_dict(previous_control.to_dict())
current_peers.append(fresh.as_reader())
current_state = messaging.new_message('carState', valid=True, logMonoTime=now)
current_state.carState = cs
with patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', False), \
patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one', return_value=current_state.as_reader()), \
patch.object(sd.sm, 'update', side_effect=lambda timeout, now=now, current_peers=current_peers, sd=sd:
sd.sm.update_msgs(now / 1e9, current_peers)):
sd.step()
maximum_mismatch = max(maximum_mismatch, sd.mismatch_counter)
mismatch_observed |= EventName.controlsMismatch in sd.events.names
if 5 <= tick < 8:
self.assertTrue(sd.enabled)
if tick >= 8:
self.assertFalse(libsafety_py.libsafety.get_controls_allowed())
self.assertEqual(maximum_mismatch, 200)
self.assertTrue(mismatch_observed)
self.assertFalse(sd.enabled)