From b8d223d6ff031afedeeac42dccab9983fd39322e Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Tue, 10 Nov 2026 00:16:26 -0600 Subject: [PATCH] Reuse exact mixed Hyundai configuration qualification on the control path --- .../car/hyundai/blended_longitudinal.py | 25 +++++- .../tests/test_mixed_qualification_cache.py | 82 +++++++++++++++++++ prebuilt.json | 2 +- 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 opendbc_repo/opendbc/car/hyundai/tests/test_mixed_qualification_cache.py diff --git a/opendbc_repo/opendbc/car/hyundai/blended_longitudinal.py b/opendbc_repo/opendbc/car/hyundai/blended_longitudinal.py index 0432ded48a..67261c9219 100644 --- a/opendbc_repo/opendbc/car/hyundai/blended_longitudinal.py +++ b/opendbc_repo/opendbc/car/hyundai/blended_longitudinal.py @@ -5,6 +5,9 @@ Shared Ioniq and generic Hyundai initialization are intentionally independent. """ from dataclasses import dataclass from enum import Enum +from functools import lru_cache + +from capnp.lib.capnp import _DynamicStructBuilder, _DynamicStructReader from opendbc.car.hyundai.values import CAR, is_blended, HyundaiFlags, HyundaiSafetyFlags @@ -194,7 +197,7 @@ class BlendedLongitudinalController: return messages -def mixed_startup_qualified(cp, *, is_release=False, allow_marked=False): +def _mixed_startup_qualified_uncached(cp, *, is_release=False, allow_marked=False): """Exact prepared mixed owner; stock topology remains immutable.""" from opendbc.car import structs from opendbc.car.hyundai.hyundaicanfd import CanBus @@ -538,3 +541,23 @@ def startup_owner(cp, callbacks, *, requested): candidate = candidate_from_stock(cp, alpha_requested=requested, native_qualified=cp.alphaLongitudinalAvailable and mixed_startup_qualified(cp)) return None if candidate is None else BlendedStartup(cp, candidate, callbacks) + + +@lru_cache(maxsize=32) +def _mixed_startup_for_bytes(key, is_release, allow_marked): + from opendbc.car import structs + with structs.CarParams.from_bytes(key) as cp: + return _mixed_startup_qualified_uncached(cp, is_release=is_release, allow_marked=allow_marked) + + +def mixed_startup_qualified(cp, *, is_release=False, allow_marked=False): + from opendbc.car import structs + if type(cp) in (_DynamicStructBuilder, _DynamicStructReader) and cp.schema == structs.CarParams.schema: + try: + snapshot = cp.copy() if type(cp) is _DynamicStructBuilder else cp.as_builder() + key = snapshot.to_bytes() + if len(key) <= 65536: + return _mixed_startup_for_bytes(key, is_release, allow_marked) + except Exception: + pass + return _mixed_startup_qualified_uncached(cp, is_release=is_release, allow_marked=allow_marked) diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_mixed_qualification_cache.py b/opendbc_repo/opendbc/car/hyundai/tests/test_mixed_qualification_cache.py new file mode 100644 index 0000000000..9da237de89 --- /dev/null +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_mixed_qualification_cache.py @@ -0,0 +1,82 @@ +from unittest.mock import patch + +import pytest + +from opendbc.car.hyundai import blended_longitudinal as mixed +from opendbc.car.hyundai.tests.test_blended_stock_aol import params + + +@pytest.fixture(params=(False, True)) +def cp(request): + cache = getattr(mixed, '_mixed_startup_for_bytes', None) + if cache is not None: + cache.cache_clear() + return params(request.param) + + +def test_same_static_configuration_is_qualified_once(cp): + before = cp.to_dict() + with patch.object(mixed, 'alpha_eligible', wraps=mixed.alpha_eligible) as evaluate: + for _ in range(20): + assert mixed.mixed_startup_qualified(cp) + assert mixed.mixed_startup_qualified(cp.as_reader()) + assert evaluate.call_count == 1 + assert cp.to_dict() == before + + +@pytest.mark.parametrize('field', ('flags', 'brand', 'carFingerprint', 'passive', 'dashcamOnly', 'notCar', + 'steerControlType', 'alternativeExperience', 'safetyConfigs', 'safetyModel', + 'safetyParam', 'pcmCruise', 'openpilotLongitudinalControl', 'mass', 'carVin', 'carFw')) +def test_changes_on_same_builder_are_requalified(cp, field): + uncached = getattr(mixed, '_mixed_startup_qualified_uncached', mixed.mixed_startup_qualified) + assert mixed.mixed_startup_qualified(cp) + if field in ('passive', 'dashcamOnly', 'notCar', 'pcmCruise', 'openpilotLongitudinalControl'): + setattr(cp, field, not getattr(cp, field)) + elif field in ('flags', 'alternativeExperience'): + setattr(cp, field, int(getattr(cp, field)) ^ 1) + elif field == 'safetyConfigs': + cp.safetyConfigs = [] + elif field == 'safetyParam': + cp.safetyConfigs[0].safetyParam ^= 1 + elif field == 'safetyModel': + cp.safetyConfigs[0].safetyModel = 'noOutput' + elif field == 'steerControlType': + cp.steerControlType = 'angle' + elif field == 'mass': + cp.mass += 1 + elif field == 'carFw': + cp.carFw = [{'ecu': 'eps', 'fwVersion': b'changed'}] + else: + setattr(cp, field, 'changed') + with patch.object(mixed, 'alpha_eligible', wraps=mixed.alpha_eligible) as evaluate: + for release in (False, True): + for marked in (False, True): + assert mixed.mixed_startup_qualified(cp, is_release=release, allow_marked=marked) == uncached(cp, is_release=release, allow_marked=marked) + assert evaluate.call_count > 0 + + +@pytest.mark.parametrize('longitudinal', (False, True)) +@pytest.mark.parametrize('marked', (False, True)) +def test_stock_long_and_aol_profiles_retain_release_restriction(cp, longitudinal, marked): + cp.openpilotLongitudinalControl = longitudinal + cp.pcmCruise = not longitudinal + if longitudinal: + cp.safetyConfigs[0].safetyParam |= 4 + cp.alternativeExperience = 32 if marked else 0 + assert mixed.mixed_startup_qualified(cp, allow_marked=marked) + assert not mixed.mixed_startup_qualified(cp, is_release=True, allow_marked=marked) + if marked: + assert not mixed.mixed_startup_qualified(cp) + + +def test_proxy_and_snapshot_failure_retain_original_validation(cp): + class Proxy: + def __getattr__(self, name): + return getattr(cp, name) + assert mixed.mixed_startup_qualified(Proxy()) + cached = getattr(mixed, '_mixed_startup_for_bytes', None) + if cached is not None: + with patch.object(mixed, '_mixed_startup_for_bytes', side_effect=RuntimeError('cache unavailable')): + assert mixed.mixed_startup_qualified(cp) + cp.passive = True + assert not mixed.mixed_startup_qualified(cp) diff --git a/prebuilt.json b/prebuilt.json index 92bb2a536b..cdc3a9cccf 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "cd934dc89ad38cfecee7994f992b3f426038c914fa9184b8c7e835b349485acd", "schema_version": 1} +{"protected_inventory_sha256": "745786774cd0e323f79d7003c30cbacd2a576521102175b8188f790902d142ba", "schema_version": 1}