From e4ec6c1a1432e9e2d7ae928b9c899cec8274ddeb Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:45:58 -0500 Subject: [PATCH] Guard Volt CC pedal discovery against partial camera layouts --- opendbc_repo/opendbc/car/gm/fingerprints.py | 4 + opendbc_repo/opendbc/car/gm/interface.py | 13 +- .../car/gm/tests/test_volt_cc_automatic.py | 132 +++++++++++++- .../gm/tests/test_volt_cc_camera_topology.py | 163 ++++++++++++++++++ 4 files changed, 305 insertions(+), 7 deletions(-) create mode 100644 opendbc_repo/opendbc/car/gm/tests/test_volt_cc_camera_topology.py diff --git a/opendbc_repo/opendbc/car/gm/fingerprints.py b/opendbc_repo/opendbc/car/gm/fingerprints.py index 2c753ed920..934a1cac93 100644 --- a/opendbc_repo/opendbc/car/gm/fingerprints.py +++ b/opendbc_repo/opendbc/car/gm/fingerprints.py @@ -10,6 +10,10 @@ FINGERPRINTS = { # Dom no-ACC Volt: only this signature has the complete existing CC source layout. CAR.CHEVROLET_VOLT_CC: [{ 201: 8, 493: 8, 495: 4, 193: 8, 197: 8, 209: 7, 171: 8, 456: 8, 199: 4, 489: 8, 211: 2, 499: 3, 390: 7, 532: 6, 568: 1, 761: 7, 381: 6, 485: 8, 189: 7, 479: 3, 711: 6, 501: 8, 241: 6, 717: 5, 869: 4, 389: 2, 454: 8, 170: 8, 190: 6, 497: 8, 417: 7, 419: 1, 426: 7, 451: 8, 452: 8, 453: 6, 500: 6, 508: 8, 528: 4, 647: 3, 1105: 6, 1005: 6, 481: 7, 844: 8, 866: 4, 564: 5, 969: 8, 388: 8, 352: 5, 562: 8, 961: 8, 386: 8, 707: 8, 977: 8, 979: 7, 298: 8, 840: 5, 842: 5, 988: 6, 1001: 8, 560: 8, 546: 7, 558: 8, 309: 8, 995: 7, 311: 8, 566: 5, 567:3, 989: 8, 384: 4, 800: 6, 1033: 7, 1034: 7, 313: 8, 554: 3, 810: 8, 1017: 8, 1019: 2, 1020: 8, 1217: 8, 1223: 3, 1233: 8, 1227: 4, 1417: 8, 1009: 8, 1221: 5, 1275: 3, 1225: 7, 289: 8, 550: 8, 1273: 3, 1928: 7, 1187: 4, 1265: 8, 1927: 7, 1267: 1, 1906: 7, 288: 5, 304: 1, 328: 1, 1912: 7, 320: 3, 1910: 7, 563: 5, 1249: 8, 1930: 7, 1257: 6, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 565: 5, 1280: 4, 1907: 7 + }, + # Dom Volt CC pedal, F1 brake and camera removed: existing E603/E613 owner. + { + 189: 7, 193: 8, 197: 8, 201: 8, 209: 7, 211: 2, 241: 6, 288: 5, 298: 8, 304: 1, 308: 4, 309: 8, 311: 8, 313: 8, 320: 3, 328: 1, 352: 5, 381: 6, 384: 4, 386: 8, 388: 8, 451: 8, 452: 8, 453: 6, 479: 3, 481: 7, 485: 8, 489: 8, 493: 8, 497: 8, 500: 6, 501: 8, 513: 6, 528: 4, 532: 6, 560: 8, 562: 8, 563: 5, 565: 5, 566: 5, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 707: 8, 717: 5, 761: 7, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 869: 4, 977: 8, 1001: 8, 1017: 8, 1020: 8, 1033: 7, 1034: 7, 1217: 8, 1221: 5, 1233: 8, 1249: 8, 1265: 8, 1267: 1, 1280: 4, 1300: 8, 1922: 7, 1930: 7 }], CAR.CHEVROLET_MALIBU_HYBRID_CC: [{193: 8, 197: 8, 201: 8, 209: 7, 211: 2, 241: 6, 249: 8, 352: 5, 386: 8, 451: 8, 452: 8, 453: 6, 481: 7, 485: 8, 489: 8, 493: 8, 500: 6, 560: 8, 562: 8, 566: 6, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 707: 8, 717: 5, 761: 7, 810: 8, 840: 5, 842: 5, 844: 8, 869: 4}], CAR.HOLDEN_ASTRA: [{ diff --git a/opendbc_repo/opendbc/car/gm/interface.py b/opendbc_repo/opendbc/car/gm/interface.py index 4429f0d70a..d4eb931af3 100755 --- a/opendbc_repo/opendbc/car/gm/interface.py +++ b/opendbc_repo/opendbc/car/gm/interface.py @@ -732,21 +732,26 @@ class CarInterface(CarInterfaceBase): cam = fingerprint.get(CanBus.CAMERA, {}) be = pt.get(0xBE) in (6, 7, 8) f1 = 0xBE not in pt and pt.get(0xF1) == 6 - removed = 0x320 not in cam + # Any known camera-owner frame rules out the removed-camera topology. + removed = not any(address in cam for address in (0x180, 0x320, 0x370)) + camera_layout = removed or (cam.get(0x180) == 4 and cam.get(0x320) == 6 and + cam.get(0x370, 6) == 6) required = {0x184: 8, 0x34A: 5, 0x1E1: 7, 0x1C4: 8, 0xC9: 8, 0x3D1: 8, 0xBD: 7} sources = (be or f1) and all(pt.get(a) == n for a, n in required.items()) ret.radarUnavailable = RADAR_HEADER_MSG not in fingerprint[CanBus.OBSTACLE] and CAMERA_DATA_HEADER_MSG not in fingerprint[CanBus.OBSTACLE] ret.flags = int(GMFlags.PEDAL_LONG | (GMFlags.NO_CAMERA if removed else 0) | (GMFlags.NO_ACCELERATOR_POS_MSG if f1 else 0)) | (int(ret.flags) & int(GMFlags.HAS_BSM)) ret.networkLocation = NetworkLocation.fwdCamera - ret.dashcamOnly = not sources or (not removed and (cam.get(0x180) != 4 or cam.get(0x320) != 6)) + ret.dashcamOnly = not sources or not camera_layout ret.alphaLongitudinalAvailable = not ret.dashcamOnly and not is_release and pt.get(0x1F5) == 8 ret.openpilotLongitudinalControl = ret.alphaLongitudinalAvailable ret.pcmCruise = False if not ret.dashcamOnly: ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.gm)] - ret.safetyConfigs[0].safetyParam = ((0xE600 if ret.openpilotLongitudinalControl else 0xE610) + - 4 * int(not ret.radarUnavailable) + 2 * int(removed) + int(f1)) + ret.safetyConfigs[0].safetyParam = ((0xE600 if ret.openpilotLongitudinalControl else 0xE610) + + 4 * int(not ret.radarUnavailable) + 2 * int(removed) + int(f1)) + else: + ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.noOutput)] ret.autoResumeSng = ret.openpilotLongitudinalControl ret.minEnableSpeed = -1. ret.stopAccel = -1.5 diff --git a/opendbc_repo/opendbc/car/gm/tests/test_volt_cc_automatic.py b/opendbc_repo/opendbc/car/gm/tests/test_volt_cc_automatic.py index f8f7a18558..54fe94d173 100644 --- a/opendbc_repo/opendbc/car/gm/tests/test_volt_cc_automatic.py +++ b/opendbc_repo/opendbc/car/gm/tests/test_volt_cc_automatic.py @@ -14,11 +14,11 @@ from opendbc.safety.tests.libsafety import libsafety_py class TestVoltCcAutomatic(unittest.TestCase): - def discovered(self, release=False): + def discovered(self, release=False, *, variant=0): variants = FINGERPRINTS[CAR.CHEVROLET_VOLT_CC] - self.assertEqual(len(variants), 1) + self.assertEqual(len(variants), 2) messages = [SimpleNamespace(address=address, dat=bytes(length), src=0) - for address, length in variants[0].items()] + for address, length in variants[variant].items()] selected, observed = can_fingerprint(lambda **_kwargs: [messages]) self.assertEqual(selected, CAR.CHEVROLET_VOLT_CC) cp = CarInterface.get_params(selected, observed, [], False, release, False) @@ -100,6 +100,132 @@ class TestVoltCcAutomatic(unittest.TestCase): self.assertFalse(command.latActive) self.assertFalse(command.longActive) + def test_automatic_pedal_signature_keeps_distinct_brake_and_off_owner(self): + from opendbc.car.gm.startup_preferences import prepare_disable_longitudinal + from opendbc.car.gm.values import volt_cc_pedal_profile + for release in (False, True): + cp, ci, _ = self.discovered(release, variant=1) + self.assertFalse(cp.dashcamOnly) + self.assertFalse(cp.pcmCruise) + self.assertFalse(cp.flags & GMFlags.CC_LONG) + self.assertTrue(cp.flags & GMFlags.PEDAL_LONG) + self.assertTrue(cp.flags & GMFlags.NO_ACCELERATOR_POS_MSG) + self.assertEqual(cp.openpilotLongitudinalControl, not release) + self.assertEqual(cp.safetyConfigs[0].safetyParam, 0xE613 if release else 0xE603) + self.assertIsNotNone(volt_cc_pedal_profile(cp)) + self.assertIsNotNone(ci.CC.volt_cc_pedal_profile) + prepare_disable_longitudinal(cp, True) + self.assertFalse(cp.openpilotLongitudinalControl) + self.assertEqual(cp.safetyConfigs[0].safetyParam, 0xE613) + + def test_discovered_pedal_runs_existing_actual_parser_and_wire_sequence(self): + from opendbc.can import CANPacker, CANParser + from opendbc.car import Bus + from opendbc.car.gm import gmcan + from opendbc.car.gm.tests.test_cc_gateway_stock import pt_frames + from openpilot.common.params import Params + from openpilot.common.prefix import OpenpilotPrefix + from openpilot.selfdrive.car.car_events import CarEvents, EventName + from openpilot.starpilot.controller_extensions import configure_controller + for be, removed, radar, release in ((False, True, False, False), (False, True, False, True)): + with self.subTest(be=be, removed=removed, radar=radar, release=release), OpenpilotPrefix(): + params = Params() + params.put_bool('OpenpilotEnabledToggle', True, block=True) + cp = self.discovered(release, variant=1)[0] + ci = CarInterface(cp) + configure_controller(ci, params) + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + output = CANParser(DBC[cp.carFingerprint][Bus.pt], [('GAS_COMMAND', float('nan'))], 0) + command = structs.CarControl(enabled=True, longActive=not release) + command.actuators.accel = .5 + loopback = [] + for tick in range(104): + now = 1_000_000_000 + tick * 10_000_000 + frames = [frame for frame in pt_frames(packer, counter=tick % 4, acc_cruise=6) if frame[0] not in (0xBE, 0xF1)] + frames.append(packer.make_can_msg('ECMAcceleratorPos' if be else 'EBCMBrakePedalPosition', 0, {})) + frames += [packer.make_can_msg('ECMCruiseControl', 0, {'CruiseActive': 1, 'CruiseSetSpeed': 80}), + packer.make_can_msg('EBCMRegenPaddle', 0, {})] + if not removed: + frames += [packer.make_can_msg('ASCMLKASteeringCmd', 2, {'RollingCounter': tick % 4}), + packer.make_can_msg('AEBCmd', 2, {})] + if not release and tick % 2 == 0: + sensor = bytearray.fromhex('0279012a0000') + sensor[4] = tick // 2 % 16 + sensor[5] = gmcan.pedal_crc(sensor) + frames.append((0x201, bytes(sensor), 0)) + state = ci.update([(now, frames + loopback)]) + actuators, tx = ci.apply(command.as_reader(), now) + loopback = [(addr, data, 128) for addr, data, _ in tx if addr == 0x180] + keepalives = [addr for addr, _, _ in tx if addr in (0x409, 0x40A)] + if release: + self.assertEqual(keepalives, []) + if tick == 100: + self.assertTrue(state.canValid) + self.assertEqual(keepalives, [0x409, 0x40A] if removed and not release else []) + if tick == 60: + self.assertTrue(state.canValid) + self.assertTrue(state.cruiseState.enabled) + self.assertAlmostEqual(state.cruiseState.speed, 80 / 3.6, places=4) + self.assertFalse(state.cruiseState.nonAdaptive) + events = CarEvents(cp).update(state.as_reader(), structs.CarState().as_reader(), command.as_reader()) + self.assertNotIn(EventName.wrongCruiseMode, events.names) + self.assertFalse(state.accFaulted) + if release: + self.assertFalse(any(a == 0x200 for a, _, _ in tx)) + else: + pedal = next(frame for frame in tx if frame[0] == 0x200) + output.update([(now, [pedal])]) + self.assertGreater(actuators.gas, 0.) + self.assertEqual(actuators.brake, 0.) + self.assertAlmostEqual(output.vl['GAS_COMMAND']['GAS_COMMAND'] / 255., actuators.gas, delta=.001) + if removed: + self.assertNotIn(0x180, ci.can_parsers[Bus.pt].vl) + self.assertNotIn(0x180, ci.can_parsers[Bus.cam].vl) + + def test_automatic_pedal_card_respects_saved_longitudinal_off(self): + import json + import os + + from opendbc.car import car_helpers + from opendbc.car.can_definitions import CanData + from openpilot.common.params import Params + from openpilot.common.prefix import OpenpilotPrefix + from openpilot.selfdrive.car.card import Car + from openpilot.selfdrive.controls.controlsd import Controls + from openpilot.starpilot.lateral.tests.test_lane_runtime import feed + from openpilot.starpilot.vehicle_selection import encode + + physical = FINGERPRINTS[CAR.CHEVROLET_VOLT_CC][1] + packets = [CanData(address, bytes(length), 0) for address, length in physical.items()] + + def receive(wait_for_one=False): + return [packets] if wait_for_one else [] + + def discover(*args, **kwargs): + return car_helpers.get_car(receive, *args[1:], **kwargs) + with OpenpilotPrefix(), patch.dict(os.environ, {'SKIP_FW_QUERY': '1', 'FINGERPRINT': '', 'SIMULATION': '1', 'REPLAY': '1'}): + settings = Params() + settings.put_bool('OpenpilotEnabledToggle', True, block=True) + settings.put_bool('IsReleaseBranch', False, block=True) + settings.put_bool('DisableOpenpilotLongitudinal', True, block=True) + settings.put('VehicleSelection', json.loads(encode(None)), block=True) + with patch('openpilot.selfdrive.car.card.messaging.recv_one_retry', return_value=SimpleNamespace(can=[1])), \ + patch('openpilot.selfdrive.car.card.get_car', side_effect=discover): + card = Car() + self.assertEqual(card.CP.carFingerprint, CAR.CHEVROLET_VOLT_CC) + self.assertEqual(card.CP.fingerprintSource, structs.CarParams.FingerprintSource.can) + self.assertIsNotNone(card.CI.CC.volt_cc_pedal_profile) + self.assertFalse(card.CP.openpilotLongitudinalControl) + self.assertFalse(card.CP.dashcamOnly) + controls = Controls() + self.assertEqual(controls.CP.carFingerprint, card.CP.carFingerprint) + self.assertEqual(controls.CP.safetyConfigs[0].safetyParam, 0xE613) + feed(controls, 1_000_000_000, 0, active=False, enabled=False, can_valid=False, can_timeout=True) + command, _ = controls.state_control() + self.assertFalse(command.enabled) + self.assertFalse(command.latActive) + self.assertFalse(command.longActive) + if __name__ == '__main__': unittest.main() diff --git a/opendbc_repo/opendbc/car/gm/tests/test_volt_cc_camera_topology.py b/opendbc_repo/opendbc/car/gm/tests/test_volt_cc_camera_topology.py new file mode 100644 index 0000000000..7b2ddf3f9b --- /dev/null +++ b/opendbc_repo/opendbc/car/gm/tests/test_volt_cc_camera_topology.py @@ -0,0 +1,163 @@ +"""Volt CC pedal camera ownership comes from complete observed contracts.""" +import copy +import json + +import pytest +from opendbc.can import CANPacker +from opendbc.car import Bus, gen_empty_fingerprint, structs +from opendbc.car.gm import gmcan +from opendbc.car.gm.aol import qualified_gm +from opendbc.car.gm.interface import CarInterface +from opendbc.car.gm.startup_preferences import prepare_disable_longitudinal +from opendbc.car.gm.tests.test_cc_gateway_stock import pt_frames +from opendbc.car.gm.values import CAR, DBC, GMFlags, volt_cc_pedal_profile +from opendbc.safety.tests.libsafety import libsafety_py + +BAD_CAMERAS = ( + {0x180: 4}, {0x180: 0}, {0x180: 8}, {0x320: 6}, {0x320: 0}, {0x320: 8}, + {0x370: 6}, {0x370: 0}, {0x370: 8}, {0x180: 4, 0x370: 6}, + {0x180: 4, 0x320: 8}, {0x180: 8, 0x320: 6}, {0x180: 4, 0x320: 6, 0x370: 8}, +) + + +def observed(camera, *, be=False, radar=False): + fingerprint = gen_empty_fingerprint() + fingerprint[0].update({0x184: 8, 0x34A: 5, 0x348: 5, 0x1E1: 7, 0x1C4: 8, 0xC9: 8, + 0xBD: 7, 0x232: 8, 0x3D1: 8, 0x1F5: 8, 0x201: 6, + 0xBE if be else 0xF1: 6}) + fingerprint[2].update(camera) + if radar: + from opendbc.car.gm.radar_interface import RADAR_HEADER_MSG + fingerprint[1][RADAR_HEADER_MSG] = 8 + return fingerprint + + +def params(fingerprint, *, release=False, off=False): + cp = CarInterface.get_params(CAR.CHEVROLET_VOLT_CC, fingerprint, [], False, release, False) + prepare_disable_longitudinal(cp, off) + return cp + + +@pytest.mark.parametrize('camera', BAD_CAMERAS) +@pytest.mark.parametrize('be', (False, True)) +@pytest.mark.parametrize('release', (False, True)) +def test_partial_or_malformed_camera_never_selects_removed_native_owner(camera, be, release, record_property): + cp = params(observed(camera, be=be), release=release) + record_property('topology_witness', json.dumps({'camera': sorted(camera.items()), 'be': be, 'release': release, + 'dashcam': cp.dashcamOnly, 'param': cp.safetyConfigs[0].safetyParam, + 'longitudinal': cp.openpilotLongitudinalControl})) + # BE inputs also satisfy the older nonpedal word20 branch: denial must replace it. + for off in (False, True): + prepare_disable_longitudinal(cp, off) + assert cp.dashcamOnly and not cp.openpilotLongitudinalControl + assert not cp.alphaLongitudinalAvailable and not cp.autoResumeSng + assert cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.noOutput + assert cp.safetyConfigs[0].safetyParam == 0 + assert not cp.flags & GMFlags.NO_CAMERA + assert volt_cc_pedal_profile(cp) is None and not qualified_gm(cp) + ci = CarInterface(cp) + assert ci.CC.volt_cc_pedal_profile is None + packer = CANPacker(DBC[cp.carFingerprint][Bus.pt]) + frames = pt_frames(packer, counter=0, acc_cruise=6) + ci.update([(1_000_000_000, frames)]) + control = structs.CarControl(enabled=True, latActive=True, longActive=True) + control.actuators.accel = .5 + _, commands = ci.apply(control.as_reader(), 1_000_000_001) + assert not any(address == 0x200 for address, _, _ in commands) + safety = libsafety_py.libsafety + safety.init_tests() + assert safety.set_safety_hooks(int(cp.safetyConfigs[0].safetyModel.raw), cp.safetyConfigs[0].safetyParam) == 0 + address, data, bus = gmcan.create_pedal_command(packer, .2, 0) + assert not safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data)) + + +@pytest.mark.parametrize('camera', ({}, {0x180: 4, 0x320: 6}, {0x180: 4, 0x320: 6, 0x370: 6})) +@pytest.mark.parametrize('be', (False, True)) +@pytest.mark.parametrize('radar', (False, True)) +@pytest.mark.parametrize('release', (False, True)) +def test_complete_present_and_removed_preserve_original_owners_and_off(camera, be, radar, release): + cp = params(observed(camera, be=be, radar=radar), release=release) + index = 4 * int(radar) + 2 * int(not camera) + int(not be) + assert cp.safetyConfigs[0].safetyParam == (0xE610 if release else 0xE600) + index + assert cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.gm + assert not cp.dashcamOnly and bool(cp.flags & GMFlags.NO_CAMERA) == (not camera) + assert cp.openpilotLongitudinalControl == (not release) + assert volt_cc_pedal_profile(cp) is not None + assert CarInterface(cp).CC.volt_cc_pedal_profile is not None + native = libsafety_py.libsafety + native.init_tests() + assert native.set_safety_hooks(int(cp.safetyConfigs[0].safetyModel.raw), cp.safetyConfigs[0].safetyParam) == 0 + prepare_disable_longitudinal(cp, True) + assert cp.safetyConfigs[0].safetyParam == 0xE610 + index + assert not cp.openpilotLongitudinalControl and not cp.autoResumeSng + assert not cp.dashcamOnly and volt_cc_pedal_profile(cp) is not None + + +@pytest.mark.parametrize('camera', BAD_CAMERAS) +def test_recovery_requires_new_valid_observed_cp_without_mutating_denied_owner(camera): + original = observed(camera) + denied = params(original) + snapshot = denied.to_dict() + corrected = copy.deepcopy(original) + corrected[2] = {0x180: 4, 0x320: 6} + fresh = params(corrected) + assert fresh.openpilotLongitudinalControl and fresh.safetyConfigs[0].safetyParam == 0xE601 + assert volt_cc_pedal_profile(fresh) is not None + assert denied.to_dict() == snapshot + assert denied.dashcamOnly and denied.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.noOutput + + +@pytest.mark.parametrize('mutation', ('bus1_only', 'missing_pedal', 'bad_pedal')) +def test_unqualified_pt_or_pedal_cannot_enter_volt_cc_pedal_owner(mutation): + fingerprint = observed({}) + if mutation == 'bus1_only': + fingerprint[1], fingerprint[0] = fingerprint[0], {} + elif mutation == 'missing_pedal': + del fingerprint[0][0x201] + else: + fingerprint[0][0x201] = 5 + cp = params(fingerprint) + assert not cp.openpilotLongitudinalControl + assert volt_cc_pedal_profile(cp) is None + assert cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.noOutput + + +def test_missing_gear_keeps_valid_topology_but_denies_longitudinal(): + fingerprint = observed({}) + del fingerprint[0][0x1F5] + cp = params(fingerprint) + assert cp.safetyConfigs[0].safetyParam == 0xE613 + assert not cp.openpilotLongitudinalControl and not cp.autoResumeSng + assert volt_cc_pedal_profile(cp) is not None + + +@pytest.mark.parametrize('camera', ({}, {0x180: 4, 0x320: 6}, {0x180: 4}, {0x370: 6}, {0x180: 8, 0x320: 6})) +@pytest.mark.parametrize('release', (False, True)) +def test_automatic_pedal_literal_reaches_topology_gate(camera, release, monkeypatch): + from opendbc.car import car_helpers + from opendbc.car.can_definitions import CanData + from opendbc.car.gm.fingerprints import FINGERPRINTS + + physical = FINGERPRINTS[CAR.CHEVROLET_VOLT_CC][1] + packets = [CanData(address, bytes(length), 0) for address, length in physical.items()] + packets += [CanData(address, bytes(length), 2) for address, length in camera.items()] + + def receive(wait_for_one=False): + return [packets] if wait_for_one else [] + + monkeypatch.setenv('SKIP_FW_QUERY', '1') + monkeypatch.setenv('FINGERPRINT', '') + ci = car_helpers.get_car(receive, lambda *_args, **_kwargs: None, + lambda *_args, **_kwargs: None, False, release) + assert ci.CP.carFingerprint == CAR.CHEVROLET_VOLT_CC + assert ci.CP.fingerprintSource == structs.CarParams.FingerprintSource.can + valid = not camera or camera == {0x180: 4, 0x320: 6} + if valid: + expected = (0xE610 if release else 0xE600) + (3 if not camera else 1) + assert ci.CP.safetyConfigs[0].safetyParam == expected + assert ci.CC.volt_cc_pedal_profile is not None + else: + assert ci.CP.dashcamOnly and not ci.CP.openpilotLongitudinalControl + assert ci.CP.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.noOutput + assert ci.CP.safetyConfigs[0].safetyParam == 0 + assert ci.CC.volt_cc_pedal_profile is None