mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
Cache immutable vehicle capability policies
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
"""Exact CarParams snapshots may cache policy; live authority remains current."""
|
||||
|
||||
from dataclasses import FrozenInstanceError
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from capnp import KjException
|
||||
|
||||
from opendbc.car import gen_empty_fingerprint, structs
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
from opendbc.car.ford.values import CAR as FORD
|
||||
from opendbc.car.gm.values import CAR as GM
|
||||
from opendbc.car.honda.values import CAR as HONDA
|
||||
from opendbc.car.hyundai.values import CAR as HYUNDAI
|
||||
from opendbc.car.mazda.values import CAR as MAZDA
|
||||
from opendbc.car.tesla.fingerprints import FINGERPRINTS as TESLA_FINGERPRINTS
|
||||
from opendbc.car.tesla.values import CAR as TESLA
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||
from openpilot.starpilot.aol import vehicle
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_policy_cache():
|
||||
vehicle._policy_for_bytes.cache_clear()
|
||||
yield
|
||||
vehicle._policy_for_bytes.cache_clear()
|
||||
|
||||
|
||||
def profile(brand):
|
||||
"""Factory inputs from each port's existing positive admission fixtures."""
|
||||
models = {'honda': HONDA.HONDA_CIVIC_BOSCH, 'hyundai': HYUNDAI.HYUNDAI_SONATA,
|
||||
'gm': GM.CHEVROLET_BOLT_CC_2018_2021, 'ford': FORD.FORD_ESCAPE_MK4,
|
||||
'mazda': MAZDA.MAZDA_CX5_2022, 'tesla': TESLA.TESLA_MODEL_S_PREAP,
|
||||
'toyota': TOYOTA.TOYOTA_COROLLA_TSS2}
|
||||
fp = gen_empty_fingerprint()
|
||||
if brand == 'gm':
|
||||
fp[0][0x201] = 6
|
||||
fp[2].update({0x320: 8, 0x180: 4})
|
||||
elif brand == 'ford':
|
||||
fp[0][0x5A] = 8
|
||||
fp[2].update({0x3D6: 8, 0x186: 8})
|
||||
elif brand == 'tesla':
|
||||
fp[0] = dict(TESLA_FINGERPRINTS[models[brand]][0])
|
||||
return interfaces[models[brand]].get_params(models[brand], fp, [], brand == 'honda', False, False)
|
||||
|
||||
|
||||
def assert_equivalent(cp):
|
||||
expected = vehicle._policy_for_uncached(cp)
|
||||
assert vehicle.policy_for(cp) == expected
|
||||
assert vehicle.policy_for(cp.as_reader()) == expected
|
||||
return expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('brand', ('honda', 'hyundai', 'gm', 'ford', 'mazda', 'tesla', 'toyota'))
|
||||
def test_actual_port_profile_and_startup_marking_match(brand):
|
||||
cp = profile(brand)
|
||||
initial = assert_equivalent(cp)
|
||||
assert initial.intent_supported, brand
|
||||
cp.safetyConfigs[-1].safetyParam |= initial.safety_param_addition
|
||||
cp.alternativeExperience |= initial.alternative_experience_addition
|
||||
marked = assert_equivalent(cp)
|
||||
assert marked.intent_supported and marked.runtime_supported
|
||||
assert vehicle.native_matches_cp(cp, int(cp.safetyConfigs[-1].safetyModel.raw), cp.safetyConfigs[-1].safetyParam)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('control', ('torque', 'pid', 'angle'))
|
||||
def test_controller_variants_match(control):
|
||||
cp = profile('toyota')
|
||||
cp.steerControlType = 'angle' if control == 'angle' else 'torque'
|
||||
cp.lateralTuning.init('pid' if control == 'pid' else 'torque')
|
||||
assert_equivalent(cp)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field', ('brand', 'carFingerprint', 'flags', 'passive', 'notCar', 'dashcamOnly',
|
||||
'pcmCruise', 'openpilotLongitudinalControl', 'alternativeExperience',
|
||||
'safetyParam', 'safetyModel', 'safetyConfigs', 'lateralTuning', 'carVin'))
|
||||
def test_mutable_builder_and_alias_reader_never_reuse_a_prior_snapshot(field):
|
||||
cp = profile('toyota')
|
||||
alias = cp.as_reader()
|
||||
accepted = vehicle.policy_for(cp)
|
||||
assert accepted.intent_supported
|
||||
before_key = vehicle._car_params_bytes(cp)
|
||||
before = vehicle._policy_for_bytes.cache_info()
|
||||
if field == 'safetyParam':
|
||||
cp.safetyConfigs[0].safetyParam ^= 1
|
||||
elif field == 'safetyModel':
|
||||
cp.safetyConfigs[0].safetyModel = 'noOutput'
|
||||
elif field == 'safetyConfigs':
|
||||
cp.safetyConfigs = []
|
||||
elif field == 'lateralTuning':
|
||||
cp.lateralTuning.init('pid')
|
||||
elif field in ('passive', 'notCar', 'dashcamOnly', 'pcmCruise', 'openpilotLongitudinalControl'):
|
||||
setattr(cp, field, not getattr(cp, field))
|
||||
else:
|
||||
setattr(cp, field, {'brand': 'unregistered', 'carFingerprint': 'unregistered', 'flags': int(cp.flags) ^ 1,
|
||||
'alternativeExperience': 1, 'carVin': 'CACHE-MUTATION'}[field])
|
||||
assert vehicle._car_params_bytes(cp) != before_key
|
||||
expected = vehicle._policy_for_uncached(cp)
|
||||
assert vehicle.policy_for(cp) == vehicle.policy_for(alias) == expected
|
||||
after = vehicle._policy_for_bytes.cache_info()
|
||||
assert after.misses == before.misses + 1
|
||||
assert after.hits == before.hits + 1
|
||||
if field in ('brand', 'carFingerprint', 'passive', 'notCar', 'dashcamOnly', 'safetyModel', 'safetyConfigs'):
|
||||
assert not expected.intent_supported
|
||||
|
||||
|
||||
def test_duck_types_and_schema_spoofs_bypass_serialization_untouched(monkeypatch):
|
||||
sentinel = vehicle.AolVehiclePolicy(settings_supported=True)
|
||||
owner = SimpleNamespace(policy_for=Mock(return_value=sentinel))
|
||||
monkeypatch.setitem(vehicle._PORTS, 'cache-test', owner)
|
||||
for schema in (None, structs.CarParams.schema):
|
||||
cp = SimpleNamespace(brand='cache-test', schema=schema, to_bytes=Mock(side_effect=AssertionError),
|
||||
copy=Mock(side_effect=AssertionError), as_builder=Mock(side_effect=AssertionError))
|
||||
assert vehicle._car_params_bytes(cp) is None
|
||||
assert vehicle.policy_for(cp) is sentinel
|
||||
owner.policy_for.assert_called_with(cp)
|
||||
cp.to_bytes.assert_not_called()
|
||||
cp.copy.assert_not_called()
|
||||
cp.as_builder.assert_not_called()
|
||||
|
||||
class HostileMeta(type):
|
||||
def __eq__(cls, other):
|
||||
raise AssertionError('Unknown type equality must not run')
|
||||
|
||||
class Unknown(metaclass=HostileMeta):
|
||||
brand = 'cache-test'
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
raise AssertionError('Unknown schema must not be read')
|
||||
|
||||
assert vehicle._car_params_bytes(Unknown()) is None
|
||||
assert vehicle.policy_for(Unknown()) is sentinel
|
||||
assert vehicle._policy_for_bytes.cache_info().misses == 0
|
||||
|
||||
|
||||
def outcome(call, cp):
|
||||
try:
|
||||
return ('result', call(cp))
|
||||
except Exception as exc:
|
||||
return ('exception', type(exc), str(exc))
|
||||
|
||||
|
||||
def test_malformed_wrong_schema_and_original_exceptions_are_preserved(monkeypatch):
|
||||
wrong = structs.CarState(canValid=True)
|
||||
for cp in (None, b'not a CarParams message', object(), SimpleNamespace(brand=[]), wrong, wrong.as_reader()):
|
||||
assert vehicle._car_params_bytes(cp) is None
|
||||
assert outcome(vehicle.policy_for, cp) == outcome(vehicle._policy_for_uncached, cp)
|
||||
owner = SimpleNamespace(policy_for=Mock(side_effect=RuntimeError('original dispatcher exception')))
|
||||
monkeypatch.setitem(vehicle._PORTS, 'cache-test', owner)
|
||||
cp = SimpleNamespace(brand='cache-test')
|
||||
assert outcome(vehicle.policy_for, cp) == outcome(vehicle._policy_for_uncached, cp)
|
||||
with pytest.raises((ValueError, KjException)):
|
||||
vehicle._policy_for_bytes(b'not a CarParams message')
|
||||
|
||||
|
||||
def test_serialization_and_decode_failure_fall_back_to_original_builder(monkeypatch):
|
||||
cp = profile('toyota')
|
||||
expected = vehicle._policy_for_uncached(cp)
|
||||
|
||||
class BrokenSchema:
|
||||
@property
|
||||
def schema(self):
|
||||
raise RuntimeError('schema unavailable')
|
||||
|
||||
with monkeypatch.context() as broken:
|
||||
broken.setattr(vehicle, 'CarParams', BrokenSchema())
|
||||
assert vehicle._car_params_bytes(cp) is None
|
||||
assert vehicle.policy_for(cp) == expected
|
||||
decoder = SimpleNamespace(schema=structs.CarParams.schema, from_bytes=Mock(side_effect=RuntimeError('decode unavailable')))
|
||||
monkeypatch.setattr(vehicle, 'CarParams', decoder)
|
||||
with patch.object(vehicle, '_policy_for_uncached', wraps=vehicle._policy_for_uncached) as uncached:
|
||||
assert vehicle.policy_for(cp) == expected
|
||||
uncached.assert_called_once_with(cp)
|
||||
decoder.from_bytes.assert_called_once()
|
||||
|
||||
|
||||
def test_original_builder_write_state_and_repeat_warning_are_unchanged():
|
||||
cp = profile('toyota')
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter('always')
|
||||
for _ in range(4):
|
||||
assert_equivalent(cp)
|
||||
assert not caught
|
||||
cp.to_bytes()
|
||||
assert not caught
|
||||
cp.to_bytes()
|
||||
assert any('already been written' in str(item.message) for item in caught)
|
||||
|
||||
|
||||
def test_lru_bound_oversize_bypass_and_frozen_result():
|
||||
cp = profile('toyota')
|
||||
for index in range(9):
|
||||
cp.carVin = str(index)
|
||||
assert_equivalent(cp)
|
||||
info = vehicle._policy_for_bytes.cache_info()
|
||||
assert info.maxsize == info.currsize == 8
|
||||
cp.carVin = '0'
|
||||
vehicle.policy_for(cp)
|
||||
assert vehicle._policy_for_bytes.cache_info().misses == info.misses + 1
|
||||
cp.carVin = 'v' * (64 * 1024)
|
||||
before = vehicle._policy_for_bytes.cache_info()
|
||||
assert vehicle._car_params_bytes(cp) is None
|
||||
result = vehicle.policy_for(cp)
|
||||
assert result == vehicle._policy_for_uncached(cp)
|
||||
assert vehicle._policy_for_bytes.cache_info() == before
|
||||
with pytest.raises(FrozenInstanceError):
|
||||
result.__setattr__('intent_supported', False)
|
||||
|
||||
|
||||
def test_warm_policy_does_not_cache_current_state_or_native_permission():
|
||||
cp = profile('mazda')
|
||||
cp.alternativeExperience = 32
|
||||
assert vehicle.policy_for(cp).ordinary_axis_ack_required
|
||||
assert vehicle.policy_for(cp).intent_supported
|
||||
cs = structs.CarState(canValid=True, canTimeout=False, gearShifter='drive', vEgo=20)
|
||||
native = SimpleNamespace(safetyModel=int(cp.safetyConfigs[0].safetyModel.raw),
|
||||
safetyParam=cp.safetyConfigs[0].safetyParam, requestedLateral=True, lateralAllowed=True)
|
||||
assert vehicle.ordinary_axis_request_allowed(cp, cs)
|
||||
assert not vehicle.native_latch_rejected(cp, native, state=cs)
|
||||
native.lateralAllowed = False
|
||||
assert vehicle.native_latch_rejected(cp, native, state=cs)
|
||||
cs.canValid = False
|
||||
assert not vehicle.ordinary_axis_request_allowed(cp, cs)
|
||||
cs.canValid, cs.steerFaultPermanent = True, True
|
||||
assert not vehicle.ordinary_axis_request_allowed(cp, cs)
|
||||
cs.steerFaultPermanent, cs.steerFaultTemporary = False, True
|
||||
assert not vehicle.native_latch_rejected(cp, native, state=cs)
|
||||
native.safetyParam ^= 1
|
||||
assert not vehicle.native_matches_cp(cp, native.safetyModel, native.safetyParam)
|
||||
@@ -1,5 +1,10 @@
|
||||
"""Fail-closed dispatch to vehicle-owned AOL capabilities."""
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from capnp.lib.capnp import _DynamicStructBuilder, _DynamicStructReader
|
||||
from opendbc.car.structs import CarParams
|
||||
|
||||
from openpilot.starpilot.aol.policy import AolVehiclePolicy
|
||||
from openpilot.starpilot.aol.intent import AolCardIntent
|
||||
from openpilot.starpilot.car.honda import aol as honda
|
||||
@@ -11,9 +16,11 @@ from openpilot.starpilot.car.tesla import aol as tesla
|
||||
from openpilot.starpilot.car.toyota import aol as toyota
|
||||
|
||||
_PORTS = {'honda': honda, 'hyundai': hyundai, 'gm': gm, 'ford': ford, 'mazda': mazda, 'tesla': tesla, 'toyota': toyota}
|
||||
_POLICY_CACHE_SIZE = 8
|
||||
_POLICY_CACHE_MAX_BYTES = 64 * 1024
|
||||
|
||||
|
||||
def policy_for(CP) -> AolVehiclePolicy:
|
||||
def _policy_for_uncached(CP) -> AolVehiclePolicy:
|
||||
try:
|
||||
port = _PORTS.get(CP.brand)
|
||||
return port.policy_for(CP) if port is not None else AolVehiclePolicy()
|
||||
@@ -21,6 +28,44 @@ def policy_for(CP) -> AolVehiclePolicy:
|
||||
return AolVehiclePolicy()
|
||||
|
||||
|
||||
def _car_params_bytes(CP) -> bytes | None:
|
||||
# Unknown ducktypes keep the original dispatch behavior without serialization.
|
||||
cp_type = type(CP)
|
||||
if cp_type is not _DynamicStructBuilder and cp_type is not _DynamicStructReader:
|
||||
return None
|
||||
try:
|
||||
if CP.schema != CarParams.schema:
|
||||
return None
|
||||
# Never serialize the original builder: to_bytes changes its write state.
|
||||
snapshot = CP.copy() if cp_type is _DynamicStructBuilder else CP.as_builder()
|
||||
if type(snapshot) is not _DynamicStructBuilder:
|
||||
return None
|
||||
key = snapshot.to_bytes()
|
||||
return key if len(key) <= _POLICY_CACHE_MAX_BYTES else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@lru_cache(maxsize=_POLICY_CACHE_SIZE)
|
||||
def _policy_for_bytes(key: bytes) -> AolVehiclePolicy:
|
||||
# Evaluate the same immutable bytes as the key, never a mutable caller alias.
|
||||
with CarParams.from_bytes(key) as CP:
|
||||
if type(CP) is not _DynamicStructReader or CP.schema != CarParams.schema:
|
||||
raise TypeError('Unsupported CarParams snapshot')
|
||||
return _policy_for_uncached(CP)
|
||||
|
||||
|
||||
def policy_for(CP) -> AolVehiclePolicy:
|
||||
key = _car_params_bytes(CP)
|
||||
if key is not None:
|
||||
try:
|
||||
# Only the frozen static policy is reused. Live authority stays uncached.
|
||||
return _policy_for_bytes(key)
|
||||
except Exception:
|
||||
pass
|
||||
return _policy_for_uncached(CP)
|
||||
|
||||
|
||||
def native_profile_supported(model: int, param: int) -> bool:
|
||||
return any(port.native_profile_supported(model, param) for port in _PORTS.values())
|
||||
|
||||
|
||||
+1
-4
@@ -1,4 +1 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"protected_inventory_sha256": "b567c57ef61d9e04942364f375a09b1465d1ce6dc5fe3e128782d2a7da622071"
|
||||
}
|
||||
{"protected_inventory_sha256": "ec474989597c90246eef09acaacab24b8bbf68c04b1271dbac521e005b84082a", "schema_version": 1}
|
||||
|
||||
Reference in New Issue
Block a user