Guard Volt CC pedal discovery against partial camera layouts

This commit is contained in:
firestar5683
2026-10-17 23:45:58 -05:00
parent 9a7ed2007e
commit e4ec6c1a14
4 changed files with 305 additions and 7 deletions
@@ -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: [{
+9 -4
View File
@@ -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
@@ -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()
@@ -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