mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
160 lines
8.2 KiB
Python
160 lines
8.2 KiB
Python
"""Actual selected Controls consumes trusted native updates without identity churn."""
|
|
from contextlib import contextmanager
|
|
import json
|
|
import os
|
|
from types import SimpleNamespace
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
from opendbc.car.car_helpers import interfaces
|
|
from opendbc.car.hyundai.values import CAR as HYUNDAI
|
|
from opendbc.car.toyota.values import CAR as TOYOTA
|
|
from openpilot.common.params import Params
|
|
from openpilot.common.prefix import OpenpilotPrefix
|
|
from openpilot.selfdrive.controls.controlsd import Controls
|
|
from openpilot.starpilot.flm.controller_replay import SelectedControllerFactory, capture_snapshot
|
|
from openpilot.starpilot.flm.learner_factory import TorqueCollectionAdapter, RAW_TORQUE
|
|
from openpilot.starpilot.flm.runtime_adapter_registry import baseline_values, baseline_revision, selected_descriptor, selected_identity
|
|
from openpilot.starpilot.flm.tests.native_controls_fixture import native_frame
|
|
from openpilot.starpilot.flm.tests.test_registered_controls_global import controls_for, published_cp
|
|
from openpilot.starpilot.lateral.controller_selection import DOCUMENT_KEY as SELECTION_KEY, ControllerMode, replace_mode
|
|
from openpilot.starpilot.lateral.learning_preferences import KEY, replace_preference
|
|
from openpilot.starpilot.lateral.torque_settings import DOCUMENT_KEY, FieldChoice, PlatformProfile, serialize_document
|
|
|
|
NOW = 400_000_000_000
|
|
CASES = [(HYUNDAI.HYUNDAI_IONIQ_6, 'starpilot', 1.22), (TOYOTA.TOYOTA_RAV4_TSS2, 'standard', 1.)]
|
|
|
|
|
|
def cp_for(vehicle):
|
|
return interfaces[vehicle].get_non_essential_params(vehicle)
|
|
|
|
|
|
@pytest.mark.parametrize('vehicle,mode,multiplier', CASES)
|
|
def test_actual_controls_65_seconds_variable_four_hz_preserves_nominal_identity_and_single_transform(vehicle, mode, multiplier):
|
|
cp = cp_for(vehicle)
|
|
with controls_for(cp, mode, True) as controls:
|
|
cp_bytes = controls.CP.as_builder().to_bytes()
|
|
anchor = tuple(controls.LaC.native_learning_baseline)
|
|
revision = controls.global_learning.baseline_revision
|
|
identity = None
|
|
for tick in range(6501):
|
|
# A bounded staircase of real4Hz native packets; all other inputs retain
|
|
# their declared-rate production SubMaster fixtures.
|
|
delta = .01 + .001 * ((tick // 25) % 8)
|
|
native_frame(controls, NOW + tick * 10_000_000, tick, native_overrides={
|
|
'factor': float(cp.lateralTuning.torque.latAccelFactor) * (1. + delta),
|
|
'friction': float(cp.lateralTuning.torque.friction) * .98,
|
|
})
|
|
runtime = controls.global_learning
|
|
assert runtime is not None
|
|
assert runtime.baseline_revision == revision
|
|
assert tuple(controls.LaC.native_learning_baseline) == anchor
|
|
row = controls.sm['lateralTorqueParameters']
|
|
assert controls.LaC.torque_params.latAccelFactor == pytest.approx(float(row.latAccelFactorFiltered) * multiplier, rel=2e-7)
|
|
assert controls.LaC.torque_params.friction == pytest.approx(float(row.frictionCoefficientFiltered), rel=2e-7)
|
|
if tick >= 50:
|
|
assert runtime.identity
|
|
identity = identity or runtime.identity
|
|
assert runtime.identity == identity
|
|
assert runtime.native_refined
|
|
assert controls.CP.as_builder().to_bytes() == cp_bytes
|
|
assert controls.params.get(KEY) == replace_preference(None, cp, True)
|
|
|
|
|
|
@pytest.mark.parametrize('vehicle,mode,multiplier', CASES)
|
|
@pytest.mark.parametrize('bad', ['inner_invalid', 'version', 'stale', 'future', 'factor', 'friction_nan'])
|
|
def test_actual_controls_reject_invalid_native_receipts(vehicle, mode, multiplier, bad):
|
|
cp = cp_for(vehicle)
|
|
with controls_for(cp, mode, True) as controls:
|
|
original = baseline_values(controls.LaC)
|
|
values = {'factor': float(cp.lateralTuning.torque.latAccelFactor) * 1.05}
|
|
if bad == 'inner_invalid':
|
|
values['valid'] = False
|
|
elif bad == 'version':
|
|
values['version'] = 2
|
|
elif bad == 'stale':
|
|
values['source_offset_ns'] = -500_000_001
|
|
elif bad == 'future':
|
|
values['source_offset_ns'] = 1
|
|
elif bad == 'factor':
|
|
values['factor'] = float(cp.lateralTuning.torque.latAccelFactor) * 2.
|
|
else:
|
|
values['friction'] = float('nan')
|
|
for tick in range(30):
|
|
native_frame(controls, NOW + tick * 10_000_000, tick, native_overrides=values)
|
|
assert baseline_values(controls.LaC) == original
|
|
assert controls.LaC.native_learning_refinement is None
|
|
|
|
|
|
@pytest.mark.parametrize('vehicle,mode,multiplier', CASES)
|
|
def test_off_keeps_historical_controller_output_despite_qualified_native_packet(vehicle, mode, multiplier):
|
|
cp = cp_for(vehicle)
|
|
with controls_for(cp, mode, False) as actual, controls_for(cp, mode, False) as reference:
|
|
original = baseline_values(actual.LaC)
|
|
for tick in range(80):
|
|
now = NOW + tick * 10_000_000
|
|
command, _ = native_frame(actual, now, tick, native_overrides={
|
|
'factor': float(cp.lateralTuning.torque.latAccelFactor) * 1.1})
|
|
baseline, _ = native_frame(reference, now, tick, native=False)
|
|
assert command.to_dict() == baseline.to_dict()
|
|
assert baseline_values(actual.LaC) == original
|
|
assert actual.LaC.native_learning_refinement is None
|
|
|
|
|
|
@contextmanager
|
|
def manual_ioniq():
|
|
cp = cp_for(HYUNDAI.HYUNDAI_IONIQ_6)
|
|
torque = cp.lateralTuning.torque
|
|
basis = (float(torque.latAccelFactor), float(torque.latAccelOffset), float(torque.friction))
|
|
document = serialize_document({str(cp.carFingerprint): PlatformProfile(basis, FieldChoice('custom', basis[0] * .95), FieldChoice())})
|
|
with OpenpilotPrefix(), mock.patch.dict(os.environ, {'REPLAY': '1', 'SIMULATION': '0', 'TORQUE_REPLAY_RUNTIME': '0',
|
|
'AOL_REPLAY_RUNTIME': '0', 'LANE_CENTERING_REPLAY_RUNTIME': '0'}), mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
|
|
params = Params()
|
|
params.put('CarParams', published_cp(cp, params).to_bytes(), block=True)
|
|
params.put(KEY, replace_preference(None, cp, True), block=True)
|
|
params.put(SELECTION_KEY, json.loads(replace_mode(None, cp, ControllerMode.STARPILOT)), block=True)
|
|
params.put(DOCUMENT_KEY, json.loads(document), block=True)
|
|
yield Controls(), document, basis
|
|
|
|
|
|
def test_actual_ioniq_manual_factor_remains_authoritative_over_native():
|
|
with manual_ioniq() as (controls, document, basis):
|
|
assert controls.torque_host is not None
|
|
for tick in range(150):
|
|
native_frame(controls, NOW + tick * 10_000_000, tick,
|
|
native_overrides={'factor': basis[0] * 1.2, 'friction': basis[2] * 1.1})
|
|
assert controls.LaC.torque_params.latAccelFactor == pytest.approx(basis[0] * .95 * 1.22, rel=2e-7)
|
|
assert controls.params.get(DOCUMENT_KEY) == json.loads(document)
|
|
assert controls.LaC.native_learning_refinement is None
|
|
|
|
|
|
@pytest.mark.parametrize('vehicle,mode,multiplier', CASES)
|
|
def test_factory_rejects_in_envelope_tune_forgery_without_native_provenance(vehicle, mode, multiplier):
|
|
cp = cp_for(vehicle)
|
|
factory = SelectedControllerFactory(cp, mode)
|
|
controller, vm = factory.create()
|
|
descriptor = selected_descriptor(cp, factory.interface_class(cp), controller)
|
|
anchor = baseline_values(controller)
|
|
revision = baseline_revision(descriptor, anchor)
|
|
epoch = 'a' * 64
|
|
snapshot = capture_snapshot(controller, vm).values
|
|
fields = dict(snapshot)
|
|
fields['tune.latAccelFactor'] *= 1.03
|
|
fields['pid.pos_limit'] *= 1.03
|
|
fields['pid.neg_limit'] *= 1.03
|
|
evidence = SimpleNamespace(stage_schema_revision=RAW_TORQUE, controller_mode=mode, turn_assist=False,
|
|
policy=descriptor.policy, controller_revision=descriptor.controller_revision, source_epoch=epoch,
|
|
baseline_revision=revision, identity=selected_identity(descriptor, epoch, revision),
|
|
replay_snapshot=tuple(fields.items()), control_ns=NOW, generation=0, effect_trace=None, effect_prepare_ns=0)
|
|
with pytest.raises(ValueError, match='checkpoint provenance'):
|
|
TorqueCollectionAdapter(cp, evidence)
|
|
# Changing nominal identity to suit forged values must fail before restore.
|
|
forged = list(anchor)
|
|
forged[1] *= 1.03
|
|
wrong_revision = baseline_revision(descriptor, tuple(forged))
|
|
evidence.baseline_revision = wrong_revision
|
|
evidence.identity = selected_identity(descriptor, epoch, wrong_revision)
|
|
with mock.patch('openpilot.starpilot.flm.controller_replay.restore_snapshot', side_effect=AssertionError('must not restore')):
|
|
with pytest.raises(ValueError, match='constructor identity'):
|
|
TorqueCollectionAdapter(cp, evidence)
|