Reuse exact mixed Hyundai configuration qualification on the control path

This commit is contained in:
firestar5683
2026-11-10 00:16:26 -06:00
parent 9deb33800a
commit b8d223d6ff
3 changed files with 107 additions and 2 deletions
@@ -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)
@@ -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)
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "cd934dc89ad38cfecee7994f992b3f426038c914fa9184b8c7e835b349485acd", "schema_version": 1}
{"protected_inventory_sha256": "745786774cd0e323f79d7003c30cbacd2a576521102175b8188f790902d142ba", "schema_version": 1}