diff --git a/opendbc_repo/opendbc/car/volkswagen/interface.py b/opendbc_repo/opendbc/car/volkswagen/interface.py index c9a7c39f64..efae827e7e 100644 --- a/opendbc_repo/opendbc/car/volkswagen/interface.py +++ b/opendbc_repo/opendbc/car/volkswagen/interface.py @@ -15,6 +15,9 @@ class CarInterface(CarInterfaceBase): @staticmethod def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + CAN = CanBus(fingerprint=fingerprint) + if CAN.pt == 4: + fingerprint = {bus: fingerprint.get(CAN.pt + bus, {}) for bus in range(4)} ret.brand = "volkswagen" ret.radarUnavailable = Bus.radar not in DBC[candidate] @@ -142,7 +145,6 @@ class CarInterface(CarInterfaceBase): ret.stopAccel = -0.55 ret.autoResumeSng = ret.minEnableSpeed == -1 - CAN = CanBus(fingerprint=fingerprint) if CAN.pt >= 4: safety_configs.insert(0, get_safety_config(structs.CarParams.SafetyModel.noOutput)) ret.safetyConfigs = safety_configs diff --git a/opendbc_repo/opendbc/car/volkswagen/stock_aol.py b/opendbc_repo/opendbc/car/volkswagen/stock_aol.py index 1598399859..fe648d7fd9 100644 --- a/opendbc_repo/opendbc/car/volkswagen/stock_aol.py +++ b/opendbc_repo/opendbc/car/volkswagen/stock_aol.py @@ -1,4 +1,4 @@ -"""Exact MQB stock-ACC capability; direct drive/system-long remain ordinary.""" +"""Exact MQB stock-ACC capability; direct drive remains ordinary.""" from opendbc.car import Bus, structs from opendbc.car.volkswagen.values import CAR, DBC, VolkswagenFlags @@ -10,20 +10,33 @@ MQB_CARS = frozenset(car for car in CAR if DBC[car].get(Bus.pt) == "vw_mqb" and KNOWN_FLAGS = int(VolkswagenFlags.STOCK_HCA_PRESENT | VolkswagenFlags.KOMBI_PRESENT | VolkswagenFlags.HAS_BSM) +def safety_config(cp): + """One owner, optionally preceded by one quiet noOutput board.""" + try: + configs = cp.safetyConfigs + if len(configs) == 1: + return configs[0] + if (len(configs) == 2 and configs[0].safetyModel == structs.CarParams.SafetyModel.noOutput and + configs[0].safetyParam == 0): + return configs[1] + except (AttributeError, TypeError, ValueError, OverflowError, IndexError): + pass + return None + + def _qualified(cp, *, marked_only=False, longitudinal=False): if (cp is None or cp.brand != "volkswagen" or cp.carFingerprint not in MQB_CARS or cp.passive or cp.notCar or cp.dashcamOnly or cp.flags & ~KNOWN_FLAGS or cp.steerControlType != structs.CarParams.SteerControlType.torque or cp.transmissionType not in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.manual) or cp.networkLocation not in (structs.CarParams.NetworkLocation.gateway, structs.CarParams.NetworkLocation.fwdCamera) or - cp.fingerprintSource == structs.CarParams.FingerprintSource.fixed or - bool(cp.openpilotLongitudinalControl) != longitudinal or bool(cp.pcmCruise) == longitudinal or len(cp.safetyConfigs) != 1 or + bool(cp.openpilotLongitudinalControl) != longitudinal or bool(cp.pcmCruise) == longitudinal or (longitudinal and (cp.networkLocation != structs.CarParams.NetworkLocation.gateway or not cp.alphaLongitudinalAvailable))): return False word = AOL_WORD | (MANUAL_WORD if cp.transmissionType == structs.CarParams.TransmissionType.manual else 0) word |= int(longitudinal) - safety = cp.safetyConfigs[0] - return (safety.safetyModel == structs.CarParams.SafetyModel.volkswagen and + safety = safety_config(cp) + return (safety is not None and safety.safetyModel == structs.CarParams.SafetyModel.volkswagen and (cp.alternativeExperience == 32 and safety.safetyParam == word or not marked_only and cp.alternativeExperience == 0 and safety.safetyParam == int(longitudinal))) diff --git a/opendbc_repo/opendbc/car/volkswagen/tests/test_mqb_aol_discovery.py b/opendbc_repo/opendbc/car/volkswagen/tests/test_mqb_aol_discovery.py index 6f8046e006..d5e20f80fd 100644 --- a/opendbc_repo/opendbc/car/volkswagen/tests/test_mqb_aol_discovery.py +++ b/opendbc_repo/opendbc/car/volkswagen/tests/test_mqb_aol_discovery.py @@ -20,7 +20,7 @@ from openpilot.starpilot.aol.vehicle import policy_for @pytest.mark.parametrize('manual', (False, True)) @pytest.mark.parametrize('gateway,alpha', ((False, False), (True, False), (True, True))) -def test_normal_golf_discovery_and_card_stock_aol_owner(manual, gateway, alpha): +def test_normal_golf_discovery_and_card_stock_aol_owner(manual, gateway, alpha, offset=0, fixed=False): fixture = {'kind': 'EXPLICIT_SYNTHETIC_FULL_ECU_DISCOVERY_INPUT_FROM_REUSED_INVENTORY_NOT_ROUTE_OR_PHYSICAL', 'identity': 'VOLKSWAGEN_GOLF_MK7', 'vin': 'WVWZZZAUZGW000000', @@ -47,15 +47,15 @@ def test_normal_golf_discovery_and_card_stock_aol_owner(manual, gateway, alpha): 'origin': 'Synthetic ECU bytes from configuration-matrix.json; actual matcher must verify uniqueness.'} fw = [structs.CarParams.CarFw(ecu=getattr(structs.CarParams.Ecu, r['ecu']), address=r['address'], subAddress=r['subAddress'] or 0, fwVersion=bytes.fromhex(r['fwVersion']), - brand='volkswagen', bus=0) for r in fixture['firmware']] + brand='volkswagen', bus=offset) for r in fixture['firmware']] exact, candidates = match_fw_to_car(fw, fixture['vin'], log=False) assert exact and candidates == {CAR.VOLKSWAGEN_GOLF_MK7}, (exact, candidates) packer = CANPacker('vw_mqb') - incoming = [packer.make_can_msg('GRA_ACC_01', 0, {'GRA_Hauptschalter': 1})] + incoming = [packer.make_can_msg('GRA_ACC_01', offset, {'GRA_Hauptschalter': 1})] if not manual: - incoming.append(packer.make_can_msg('Getriebe_11', 0, {})) + incoming.append(packer.make_can_msg('Getriebe_11', offset, {})) if gateway: - incoming.append(packer.make_can_msg('ESP_21', 1, {})) + incoming.append(packer.make_can_msg('ESP_21', offset + 1, {})) frames = [CanData(address=address, dat=data, src=bus) for address, data, bus in incoming] def receive(wait_for_one=False): @@ -77,10 +77,12 @@ def test_normal_golf_discovery_and_card_stock_aol_owner(manual, gateway, alpha): mock.patch.object(car_helpers, 'get_present_ecus', return_value=set()), mock.patch.object(car_helpers, 'get_fw_versions_ordered', return_value=fw)): # Normal public get_car: do NOT stub fingerprint, match_fw_to_car or candidate selection. - ci = car_helpers.get_car(receive, lambda *args, **kwargs: None, lambda _: None, alpha, False) + ci = car_helpers.get_car(receive, lambda *args, **kwargs: None, lambda _: None, alpha, False, + forced_candidate=CAR.VOLKSWAGEN_GOLF_MK7 if fixed else None) cp = ci.CP assert cp.carFingerprint == CAR.VOLKSWAGEN_GOLF_MK7 - assert cp.fingerprintSource == structs.CarParams.FingerprintSource.fw and not cp.fuzzyFingerprint + assert cp.fingerprintSource == (structs.CarParams.FingerprintSource.fixed if fixed else + structs.CarParams.FingerprintSource.fw) and not cp.fuzzyFingerprint assert cp.carVin == fixture['vin'] and len(cp.carFw) == len(fw) assert cp.transmissionType == (structs.CarParams.TransmissionType.manual if manual else structs.CarParams.TransmissionType.automatic) @@ -92,7 +94,11 @@ def test_normal_golf_discovery_and_card_stock_aol_owner(manual, gateway, alpha): card = Car(CI=ci, RI=ci.RadarInterface(cp)) try: assert card.aol_qualified and card.aol_card_intent is not None - assert card.CP.safetyConfigs[0].safetyParam == ((24 if manual else 8) | int(alpha)) + assert card.CP.safetyConfigs[-1].safetyParam == ((24 if manual else 8) | int(alpha)) + assert len(card.CP.safetyConfigs) == (2 if offset else 1) + if offset: + assert card.CP.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.noOutput + assert card.CP.safetyConfigs[0].safetyParam == 0 assert card.CP.alternativeExperience == 32 with structs.CarParams.from_bytes(params.get('CarParams')) as published: assert published.to_dict() == card.CP.to_dict() diff --git a/opendbc_repo/opendbc/car/volkswagen/tests/test_mqb_topology_discovery.py b/opendbc_repo/opendbc/car/volkswagen/tests/test_mqb_topology_discovery.py new file mode 100644 index 0000000000..ac56fe088b --- /dev/null +++ b/opendbc_repo/opendbc/car/volkswagen/tests/test_mqb_topology_discovery.py @@ -0,0 +1,10 @@ +"""New fixed and owner-slot discovery contexts; real matcher and public get_car.""" +import pytest +from opendbc.car.volkswagen.tests import test_mqb_aol_discovery as discovery + + +@pytest.mark.parametrize('manual', (False, True)) +@pytest.mark.parametrize('gateway,alpha', ((False, False), (True, False), (True, True))) +@pytest.mark.parametrize('offset,fixed', ((0, True), (4, False), (4, True))) +def test_topology_fixed_discovery(manual, gateway, alpha, offset, fixed): + discovery.test_normal_golf_discovery_and_card_stock_aol_owner(manual, gateway, alpha, offset, fixed) diff --git a/openpilot/selfdrive/car/card.py b/openpilot/selfdrive/car/card.py index e5c754b567..b6f18b5b60 100644 --- a/openpilot/selfdrive/car/card.py +++ b/openpilot/selfdrive/car/card.py @@ -40,7 +40,8 @@ from opendbc.car.honda.stock_aol import ( qualified as qualified_honda_stock_aol, native_observation as honda_native_observation, temporary_restriction as honda_temporary_restriction, ) -from opendbc.car.volkswagen.stock_aol import native_observation as volkswagen_native_observation +from opendbc.car.volkswagen.stock_aol import (native_observation as volkswagen_native_observation, + safety_config as volkswagen_aol_safety_config) from openpilot.starpilot.car.volkswagen.aol import (qualified as qualified_volkswagen_aol, temporary_restriction as volkswagen_temporary_restriction) from openpilot.starpilot.car.rivian.aol import retained_brake_lateral_intent as rivian_brake_intent_retained @@ -386,7 +387,8 @@ class Car: self.aol_sequence = 0 self.aol_process_fault_context = AolProcessFaultContext() if self.aol_replay else None if self.aol_qualified: - config = hyundai_aol_safety_config(self.CP) if self.CP.brand == 'hyundai' else self.CP.safetyConfigs[0] + config = (volkswagen_aol_safety_config(self.CP) if self.CP.brand == 'volkswagen' else + (hyundai_aol_safety_config(self.CP) if self.CP.brand == 'hyundai' else self.CP.safetyConfigs[0])) if config is None: raise RuntimeError('AOL requires the exact vehicle safety configuration') config.safetyParam |= aol_policy.safety_param_addition diff --git a/openpilot/starpilot/aol/tests/test_mqb_actual_callers.py b/openpilot/starpilot/aol/tests/test_mqb_actual_callers.py index e1b1deadd3..d3b9d21650 100644 --- a/openpilot/starpilot/aol/tests/test_mqb_actual_callers.py +++ b/openpilot/starpilot/aol/tests/test_mqb_actual_callers.py @@ -130,11 +130,13 @@ def test_mqb_full_engagement_brake_reverse_drive_actual_callers(gearbox, alpha, for prime in range(-110, 0): ambient_update((10_000_000_000 + prime * 10_000_000) / 1e9, []) card.slc_producer_session = 'authority-card' - class Publisher: + class Publisher(messaging.PubMaster): def __init__(self): self.messages = {} - def send(self, service, message): - self.messages[service] = messaging.log_from_bytes(message.to_bytes()) + def send(self, s, dat): + if not isinstance(dat, bytes): + dat = dat.to_bytes() + self.messages[s] = messaging.log_from_bytes(dat) card.pm = Publisher() native = libsafety_py.libsafety cleanup.callback(native.init_tests) @@ -180,7 +182,7 @@ def test_mqb_full_engagement_brake_reverse_drive_actual_callers(gearbox, alpha, if isinstance(name, str) and name in dbc.name_to_msg: if tick < 3 and name in ('TSK_06', 'LH_EPS_03'): continue # Real startup may receive other CAN sources before cruise and EPS. - fields = dict(overrides.get(name, {})) + fields: dict[str, float] = dict(overrides.get(name, {})) if 'COUNTER' in dbc.name_to_msg[name].sigs: fields['COUNTER'] = tick % 16 incoming.append(packer.make_can_msg(name, parser.bus, fields)) diff --git a/openpilot/starpilot/aol/tests/test_mqb_topology_callers.py b/openpilot/starpilot/aol/tests/test_mqb_topology_callers.py new file mode 100644 index 0000000000..223cd2f9e5 --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_mqb_topology_callers.py @@ -0,0 +1,14 @@ +"""New topology/fixed contexts through the existing actual caller harness. + +Requires source-matched native preloading; no compiler or native permission stub. +""" +import pytest +from openpilot.starpilot.aol.tests import test_mqb_actual_callers as callers + + +@pytest.mark.parametrize('gearbox', ('automatic', 'manual')) +@pytest.mark.parametrize('alpha', (False, True)) +@pytest.mark.parametrize('offset,fixed', ((0, True), (4, False), (4, True))) +@pytest.mark.parametrize('engage_on_set', (False, True)) +def test_topology_fixed_actual_callers(gearbox, alpha, offset, fixed, engage_on_set): + callers.test_mqb_full_engagement_brake_reverse_drive_actual_callers(gearbox, alpha, offset, fixed, engage_on_set) diff --git a/openpilot/starpilot/aol/tests/test_mqb_topology_contract.py b/openpilot/starpilot/aol/tests/test_mqb_topology_contract.py new file mode 100644 index 0000000000..6bc140401d --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_mqb_topology_contract.py @@ -0,0 +1,66 @@ +"""Family CP and ordered native inventory contracts for the new topology contexts.""" +from dataclasses import replace +from types import SimpleNamespace +import pytest +from opendbc.car import gen_empty_fingerprint, structs +from opendbc.car.volkswagen.interface import CarInterface +from opendbc.car.volkswagen.stock_aol import MQB_CARS, safety_config +from openpilot.starpilot.aol.runtime import native_inventory_matches_cp +from openpilot.starpilot.aol.vehicle import policy_for, native_matches_cp +from openpilot.starpilot.aol.wire import PandaSlot +from openpilot.starpilot.car.volkswagen.aol import qualified + + +def params(identity, manual, alpha, offset, fixed): + fp = gen_empty_fingerprint() + if not manual: + fp[offset][0xAD] = 8 + fp[offset + 1][0xFD] = 8 + cp = CarInterface.get_params(identity, fp, [], alpha, False, False) + cp.fingerprintSource = structs.CarParams.FingerprintSource.fixed if fixed else structs.CarParams.FingerprintSource.fw + return cp + + +@pytest.mark.parametrize('identity', sorted(MQB_CARS)) +@pytest.mark.parametrize('manual', (False, True)) +@pytest.mark.parametrize('alpha', (False, True)) +@pytest.mark.parametrize('offset,fixed', ((0, True), (4, False), (4, True))) +def test_exact_family_identity_topology_and_owner(identity, manual, alpha, offset, fixed): + cp = params(identity, manual, alpha, offset, fixed) + policy = policy_for(cp) + assert policy.intent_supported + owner = safety_config(cp) + assert owner is not None and owner.safetyModel == structs.CarParams.SafetyModel.volkswagen + owner.safetyParam |= policy.safety_param_addition + cp.alternativeExperience |= policy.alternative_experience_addition + word = (24 if manual else 8) | int(alpha) + assert qualified(cp, marked_only=True) and native_matches_cp(cp, 15, word) + assert cp.pcmCruise != alpha and cp.openpilotLongitudinalControl == alpha + if offset: + assert cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.noOutput + assert cp.safetyConfigs[0].safetyParam == 0 + assert not native_matches_cp(cp, 15, word ^ 16) + assert not native_matches_cp(cp, 34, word) + + +def test_exact_ordered_inventory_denies_incoherent_or_active_prefix(): + cp = params(sorted(MQB_CARS)[0], False, False, 4, True) + policy = policy_for(cp) + safety_config(cp).safetyParam |= policy.safety_param_addition + cp.alternativeExperience = 32 + quiet = PandaSlot(0, 'quiet', int(structs.CarParams.SafetyModel.noOutput), 0, 32, False, False, False, 0) + active = PandaSlot(1, 'owner', 15, 8, 32, True, False, False, 0) + def receipt(slots, serial='owner', stamp=123): + return SimpleNamespace(pandaInventory=slots, pandaSerial=serial, sourcePandaStatesMonoTime=stamp, + safetyModel=15, safetyParam=8) + assert native_inventory_matches_cp(receipt((quiet, active)), cp) + for slots in ((active, quiet), (quiet,), (quiet, active, active), + (replace(quiet, controlsAllowed=True), active), + (replace(quiet, safetyParam=8), active), + (replace(quiet, hardwareSerial='owner'), active), + (quiet, replace(active, heartbeatLost=True)), + (quiet, replace(active, safetyRxChecksInvalid=True))): + assert not native_inventory_matches_cp(receipt(slots), cp) + assert not native_inventory_matches_cp(receipt((quiet, active), serial='quiet'), cp) + assert not native_inventory_matches_cp(receipt((quiet, active), stamp=0), cp) + assert not native_inventory_matches_cp(receipt((), stamp=0), cp) diff --git a/openpilot/starpilot/aol/tests/test_volkswagen.py b/openpilot/starpilot/aol/tests/test_volkswagen.py index 19de51680b..6b67d55a72 100644 --- a/openpilot/starpilot/aol/tests/test_volkswagen.py +++ b/openpilot/starpilot/aol/tests/test_volkswagen.py @@ -110,7 +110,7 @@ def test_gateway_long_profile_rejects_changed_identity_bus_and_safety_context(ma changes = ( ('passive', True), ('notCar', True), ('dashcamOnly', True), ('flags', 1 << 20), ('networkLocation', structs.CarParams.NetworkLocation.fwdCamera), - ('fingerprintSource', structs.CarParams.FingerprintSource.fixed), + ('brand', 'toyota'), ('transmissionType', structs.CarParams.TransmissionType.direct), ('steerControlType', structs.CarParams.SteerControlType.angle), ('openpilotLongitudinalControl', False), ('pcmCruise', True), @@ -126,7 +126,8 @@ def test_gateway_long_profile_rejects_changed_identity_bus_and_safety_context(ma altered.safetyConfigs[0].safetyParam = word assert not long_qualified(altered) altered = structs.CarParams(**cp.to_dict()) - altered.safetyConfigs = [structs.CarParams.SafetyConfig(safetyModel=structs.CarParams.SafetyModel.noOutput), *altered.safetyConfigs] + altered.safetyConfigs = [structs.CarParams.SafetyConfig(safetyModel=structs.CarParams.SafetyModel.noOutput, safetyParam=1), + *altered.safetyConfigs] assert not long_qualified(altered) altered = structs.CarParams(**cp.to_dict()) altered.safetyConfigs[0].safetyModel = structs.CarParams.SafetyModel.volkswagenMeb diff --git a/openpilot/starpilot/aol/vehicle.py b/openpilot/starpilot/aol/vehicle.py index 756782c240..626868b39a 100644 --- a/openpilot/starpilot/aol/vehicle.py +++ b/openpilot/starpilot/aol/vehicle.py @@ -86,6 +86,10 @@ def native_matches_cp(CP, model: int, param: int) -> bool: config = honda.aol_safety_config(CP) return bool(config is not None and honda.native_accepts_cp(CP, model, param) and model == int(config.safetyModel.raw) and param == int(config.safetyParam)) + if CP.brand == 'volkswagen': + config = volkswagen.aol_safety_config(CP) + return bool(config is not None and volkswagen.native_accepts_cp(CP, model, param) and + model == int(config.safetyModel.raw) and param == int(config.safetyParam)) return bool(any(port.native_accepts_cp(CP, model, param) for port in _PORTS.values()) and model == int(CP.safetyConfigs[0].safetyModel.raw) and param == int(CP.safetyConfigs[-1].safetyParam)) diff --git a/openpilot/starpilot/car/volkswagen/aol.py b/openpilot/starpilot/car/volkswagen/aol.py index f6711feb53..6761806f6b 100644 --- a/openpilot/starpilot/car/volkswagen/aol.py +++ b/openpilot/starpilot/car/volkswagen/aol.py @@ -1,5 +1,5 @@ from opendbc.car import structs -from opendbc.car.volkswagen.stock_aol import qualified as stock_qualified, AOL_WORD, MANUAL_WORD, AOL_WORDS +from opendbc.car.volkswagen.stock_aol import qualified as stock_qualified, safety_config as aol_safety_config, AOL_WORD, MANUAL_WORD, AOL_WORDS from opendbc.car.volkswagen.long_aol import qualified as long_qualified, WORDS as LONG_WORDS from openpilot.starpilot.aol.intent import AolCardIntent, AOL_TOGGLE, ButtonType from openpilot.starpilot.aol.policy import AolVehiclePolicy @@ -31,7 +31,7 @@ def native_profile_supported(model, param): def native_accepts_cp(cp, model, param): - return qualified(cp, marked_only=True) and native_profile_supported(model, param) and param == cp.safetyConfigs[0].safetyParam + return qualified(cp, marked_only=True) and native_profile_supported(model, param) and param == aol_safety_config(cp).safetyParam def native_latch_rejected(cp, native): diff --git a/prebuilt.json b/prebuilt.json index cccba4dadf..a49b05d995 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "f40dda21dcd3a36f88cb8ea563fac27b023c7766257dcf2d36027eacb2c668d4", "schema_version": 1} +{"protected_inventory_sha256": "7ad958f6080cde1c5e80081ee1cd24927df9a703189143fcb8d15c8918cc5564", "schema_version": 1} diff --git a/tools/ci/run_vehicle_tests.py b/tools/ci/run_vehicle_tests.py index 211eda0a12..e4dcc8d830 100644 --- a/tools/ci/run_vehicle_tests.py +++ b/tools/ci/run_vehicle_tests.py @@ -71,6 +71,9 @@ AOL_LIFECYCLE_TARGETS = ( "openpilot/starpilot/aol/tests/test_volkswagen.py", "openpilot/starpilot/aol/tests/test_volkswagen_saved_intent.py", "openpilot/starpilot/aol/tests/test_mqb_actual_callers.py", + "openpilot/starpilot/aol/tests/test_mqb_topology_callers.py", + "openpilot/starpilot/aol/tests/test_mqb_topology_contract.py", + "opendbc_repo/opendbc/car/volkswagen/tests/test_mqb_topology_discovery.py", "opendbc_repo/opendbc/car/volkswagen/tests/test_mqb_aol_discovery.py", "opendbc_repo/opendbc/safety/tests/test_volkswagen_stock_aol.py", "opendbc_repo/opendbc/safety/tests/test_volkswagen_engagement_arm.py",