mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-06 20:43:46 +08:00
311 lines
16 KiB
Python
311 lines
16 KiB
Python
"""Native Corolla torque source selection, saved choices, and controller gates."""
|
|
|
|
import math
|
|
import os
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
from openpilot.cereal import messaging
|
|
from openpilot.common.params import ParamKeyFlag, Params
|
|
from openpilot.common.prefix import OpenpilotPrefix
|
|
from openpilot.selfdrive.controls.controlsd import Controls
|
|
from openpilot.selfdrive.locationd.torqued import TorqueEstimator
|
|
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed, publish_corolla_params
|
|
from openpilot.starpilot.lateral.controller_selection import DOCUMENT_KEY as CONTROLLER_KEY, ControllerMode, replace_mode
|
|
from openpilot.starpilot.lateral.torque_runtime import TorqueHost, development_enabled, read_settings
|
|
from openpilot.starpilot.lateral.torque_tuning import TorqueSource
|
|
from opendbc.car.car_helpers import interfaces
|
|
from opendbc.car.toyota.values import CAR
|
|
|
|
|
|
def corolla():
|
|
return interfaces[CAR.TOYOTA_COROLLA_TSS2].get_non_essential_params(CAR.TOYOTA_COROLLA_TSS2)
|
|
|
|
|
|
class LearnedFrame:
|
|
def __init__(self, now_ns, *, factor=2.0, offset=0.1, friction=0.1, version=1, valid=True):
|
|
self.logMonoTime = {'lateralTorqueParameters': now_ns}
|
|
self.recv_time = {'lateralTorqueParameters': now_ns / 1e9}
|
|
self.checked = True
|
|
self.state = SimpleNamespace(useParams=True, valid=valid, version=version, latAccelFactorFiltered=factor,
|
|
latAccelOffsetFiltered=offset, frictionCoefficientFiltered=friction)
|
|
|
|
def all_checks(self, _names):
|
|
return self.checked
|
|
|
|
def __getitem__(self, _name):
|
|
return self.state
|
|
|
|
|
|
class TorqueRuntimeTests(unittest.TestCase):
|
|
def test_saved_partial_precedence_and_corrupt_raw_bytes(self):
|
|
with OpenpilotPrefix():
|
|
params, cp = Params(), corolla()
|
|
host = TorqueHost(params, cp)
|
|
base = host.vehicle
|
|
self.assertIsNone(read_settings(params, base).user_friction)
|
|
params.put_bool('AdvancedLateralTune', True, block=True)
|
|
params.put('SteerLatAccel', base.lat_accel_factor * 1.2, block=True)
|
|
params.put('SteerFriction', min(1.0, base.friction * 1.2), block=True)
|
|
settings = read_settings(params, base)
|
|
self.assertTrue(settings.valid)
|
|
host.settings = settings
|
|
learned = LearnedFrame(1_000_000_000, factor=base.lat_accel_factor * 1.1,
|
|
offset=0.1, friction=base.friction * 1.1)
|
|
selected = host._select(learned, 1_000_000_000)
|
|
self.assertEqual(selected.source, TorqueSource.USER)
|
|
self.assertEqual(selected.lat_accel_factor, settings.user_factor)
|
|
self.assertEqual(selected.lat_accel_offset, base.lat_accel_offset)
|
|
self.assertEqual(selected.friction, settings.user_friction)
|
|
Path(params.get_param_path('SteerLatAccel')).write_bytes(b'nan')
|
|
self.assertFalse(read_settings(params, base).valid)
|
|
self.assertTrue(params.get_bool('AdvancedLateralTune'))
|
|
host.settings = read_settings(params, base)
|
|
self.assertEqual(host._select(learned, 1_000_000_000), base)
|
|
self.assertEqual(host.sample(learned, now_ns=1_000_000_000, lat_active=True), base)
|
|
|
|
def test_frozen_auto_synced_stock_and_individual_overrides(self):
|
|
with OpenpilotPrefix():
|
|
params, cp = Params(), corolla()
|
|
host = TorqueHost(params, cp)
|
|
base = host.vehicle
|
|
now = 1_000_000_000
|
|
learned = LearnedFrame(now, factor=base.lat_accel_factor * 1.1,
|
|
offset=0.1, friction=base.friction * 1.1)
|
|
params.put_bool('AdvancedLateralTune', True, block=True)
|
|
params.put('SteerLatAccel', base.lat_accel_factor, block=True)
|
|
params.put('SteerFriction', base.friction, block=True)
|
|
host.settings = read_settings(params, base)
|
|
self.assertIsNone(host.settings.user_factor)
|
|
self.assertIsNone(host.settings.user_friction)
|
|
self.assertEqual(host._select(learned, now).source, TorqueSource.LEARNED)
|
|
|
|
params.put('SteerLatAccel', base.lat_accel_factor * 1.2, block=True)
|
|
host.settings = read_settings(params, base)
|
|
factor_only = host._select(learned, now)
|
|
self.assertEqual(factor_only.source, TorqueSource.USER)
|
|
self.assertAlmostEqual(factor_only.lat_accel_factor, base.lat_accel_factor * 1.2)
|
|
self.assertEqual(factor_only.lat_accel_offset, base.lat_accel_offset)
|
|
self.assertAlmostEqual(factor_only.friction, base.friction * 1.1)
|
|
|
|
params.put('SteerLatAccel', base.lat_accel_factor, block=True)
|
|
params.put('SteerFriction', base.friction * 1.2, block=True)
|
|
host.settings = read_settings(params, base)
|
|
friction_only = host._select(learned, now)
|
|
self.assertEqual(friction_only.source, TorqueSource.USER)
|
|
self.assertAlmostEqual(friction_only.lat_accel_factor, base.lat_accel_factor * 1.1)
|
|
self.assertAlmostEqual(friction_only.lat_accel_offset, 0.1)
|
|
self.assertAlmostEqual(friction_only.friction, base.friction * 1.2)
|
|
|
|
# A frozen stock-sync file distinguishes an old stock save from a user edit
|
|
# after a later CarParams retune changes the current stock value.
|
|
old_stock = base.lat_accel_factor * 0.9
|
|
params.put('SteerLatAccel', old_stock, block=True)
|
|
params.put('SteerLatAccelStock', old_stock, block=True)
|
|
params.clear_all(ParamKeyFlag.CLEAR_ON_MANAGER_START)
|
|
self.assertAlmostEqual(params.get('SteerLatAccelStock'), old_stock)
|
|
host.settings = read_settings(params, base)
|
|
self.assertIsNone(host.settings.user_factor)
|
|
self.assertAlmostEqual(host._select(learned, now).lat_accel_factor, learned.state.latAccelFactorFiltered)
|
|
|
|
# Old untouched stock may be outside today's safe custom range: it is a
|
|
# stock-tracking marker, not a custom override to reject or apply.
|
|
older_stock = base.lat_accel_factor * 2.0
|
|
params.put('SteerLatAccel', older_stock, block=True)
|
|
params.put('SteerLatAccelStock', older_stock, block=True)
|
|
host.settings = read_settings(params, base)
|
|
self.assertTrue(host.settings.valid)
|
|
self.assertIsNone(host.settings.user_factor)
|
|
self.assertAlmostEqual(host._select(learned, now).lat_accel_factor, learned.state.latAccelFactorFiltered)
|
|
params.put('SteerFriction', base.friction, block=True)
|
|
params.put_bool('ForceAutoTuneOff', True, block=True)
|
|
host.settings = read_settings(params, base)
|
|
self.assertEqual(host._select(learned, now), base)
|
|
|
|
def test_malformed_saved_bool_and_numeric_settings_fail_closed(self):
|
|
with OpenpilotPrefix():
|
|
params, cp = Params(), corolla()
|
|
host = TorqueHost(params, cp)
|
|
params.put_bool('AdvancedLateralTune', True, block=True)
|
|
for key, bad in (('ForceAutoTuneOff', b'2'),
|
|
('SteerLatAccel', b'nan'), ('SteerFriction', b'inf'),
|
|
('SteerLatAccelStock', b'bad'), ('SteerFrictionStock', b'bad')):
|
|
with self.subTest(key=key):
|
|
path = Path(params.get_param_path(key))
|
|
original = path.read_bytes() if path.exists() else None
|
|
path.write_bytes(bad)
|
|
self.assertFalse(read_settings(params, host.vehicle).valid)
|
|
if original is None:
|
|
path.unlink()
|
|
else:
|
|
path.write_bytes(original)
|
|
|
|
def test_learned_source_freshness_version_and_override(self):
|
|
with OpenpilotPrefix():
|
|
params, cp = Params(), corolla()
|
|
host = TorqueHost(params, cp)
|
|
base = host.vehicle
|
|
frame = LearnedFrame(1_000_000_000, factor=base.lat_accel_factor * 1.1,
|
|
offset=0.1, friction=base.friction * 1.1)
|
|
self.assertEqual(host._select(frame, 1_000_000_000).source, TorqueSource.LEARNED)
|
|
frame.state.version = 99
|
|
self.assertEqual(host._select(frame, 1_000_000_000), base)
|
|
frame.state.version = 1
|
|
frame.logMonoTime['lateralTorqueParameters'] = 1
|
|
self.assertEqual(host._select(frame, 1_000_000_000), base)
|
|
frame.logMonoTime['lateralTorqueParameters'] = 1_000_000_000
|
|
frame.checked = False
|
|
self.assertEqual(host._select(frame, 1_000_000_000), base)
|
|
params.put_bool('AdvancedLateralTune', True, block=True)
|
|
params.put_bool('ForceAutoTuneOff', True, block=True)
|
|
host.settings = read_settings(params, base)
|
|
frame.checked = True
|
|
self.assertEqual(host._select(frame, 1_000_000_000), base)
|
|
|
|
def test_exact_car_development_policy(self):
|
|
cp = corolla()
|
|
with mock.patch.dict(os.environ, {'TORQUE_REPLAY_RUNTIME': '1'}):
|
|
self.assertTrue(development_enabled(cp))
|
|
cp.carFingerprint = 'TOYOTA_RAV4_TSS2_2023'
|
|
self.assertTrue(development_enabled(cp))
|
|
for field in ('passive', 'dashcamOnly', 'notCar'):
|
|
invalid = cp.as_reader().as_builder()
|
|
setattr(invalid, field, True)
|
|
with self.subTest(field=field):
|
|
self.assertFalse(development_enabled(invalid))
|
|
invalid = cp.as_reader().as_builder()
|
|
invalid.carFingerprint = 'UNREGISTERED_TORQUE_VEHICLE'
|
|
self.assertFalse(development_enabled(invalid))
|
|
invalid = cp.as_reader().as_builder()
|
|
invalid.lateralTuning.torque.latAccelFactor = math.nan
|
|
self.assertFalse(development_enabled(invalid))
|
|
with mock.patch.dict(os.environ, {'TORQUE_REPLAY_RUNTIME': '0'}):
|
|
self.assertFalse(development_enabled(corolla()))
|
|
|
|
def test_opt_in_learner_skips_old_cache_without_deleting_it(self):
|
|
cp = corolla()
|
|
with OpenpilotPrefix(), mock.patch('openpilot.selfdrive.locationd.torqued.get_cache', return_value=None) as get_cache:
|
|
Params().put('SteerFriction', cp.lateralTuning.torque.friction * 1.2, block=True)
|
|
with mock.patch.dict(os.environ, {'TORQUE_REPLAY_RUNTIME': '1'}):
|
|
fresh = TorqueEstimator(cp.as_reader())
|
|
self.assertEqual(get_cache.call_count, 0)
|
|
self.assertEqual(fresh.filtered_params['latAccelFactor'].x, cp.lateralTuning.torque.latAccelFactor)
|
|
cp.carFingerprint = 'TOYOTA_RAV4_TSS2_2023'
|
|
with mock.patch.dict(os.environ, {'TORQUE_REPLAY_RUNTIME': '1'}):
|
|
TorqueEstimator(cp.as_reader())
|
|
self.assertEqual(get_cache.call_count, 0)
|
|
self.assertAlmostEqual(float(Params().get('SteerFriction')), cp.lateralTuning.torque.friction * 1.2)
|
|
Params().remove('SteerFriction')
|
|
with mock.patch.dict(os.environ, {'TORQUE_REPLAY_RUNTIME': '1'}):
|
|
TorqueEstimator(cp.as_reader())
|
|
self.assertEqual(get_cache.call_count, 2)
|
|
|
|
def test_native_controls_default_equivalence_and_selected_torque(self):
|
|
with OpenpilotPrefix(), mock.patch.dict(os.environ, {'REPLAY': '1', 'AOL_REPLAY_RUNTIME': '0',
|
|
'LANE_CENTERING_REPLAY_RUNTIME': '0'}), \
|
|
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
|
|
params = Params()
|
|
publish_corolla_params(params)
|
|
with mock.patch.dict(os.environ, {'TORQUE_REPLAY_RUNTIME': '0'}):
|
|
baseline = Controls()
|
|
with mock.patch.dict(os.environ, {'TORQUE_REPLAY_RUNTIME': '1'}):
|
|
selected = Controls()
|
|
self.assertIsNone(baseline.torque_host)
|
|
self.assertIsNotNone(selected.torque_host)
|
|
for tick in range(110):
|
|
now = 1_000_000_000 + tick * 10_000_000
|
|
for controls in (baseline, selected):
|
|
feed(controls, now, tick)
|
|
a, _ = baseline.state_control()
|
|
b, _ = selected.state_control()
|
|
self.assertEqual(a.to_dict(), b.to_dict())
|
|
params.put_bool('AdvancedLateralTune', True, block=True)
|
|
native_factor = selected.torque_host.vehicle.lat_accel_factor
|
|
params.put('SteerLatAccel', native_factor * 1.3, block=True)
|
|
torque_differences = []
|
|
for tick in range(110, 230):
|
|
now = 1_000_000_000 + tick * 10_000_000
|
|
for controls in (baseline, selected):
|
|
feed(controls, now, tick)
|
|
reference, _ = baseline.state_control()
|
|
command, _ = selected.state_control()
|
|
torque_differences.append(abs(command.actuators.torque - reference.actuators.torque))
|
|
self.assertTrue(command.latActive)
|
|
self.assertGreater(selected.torque_host.applied.lat_accel_factor, native_factor)
|
|
self.assertLessEqual(selected.torque_host.applied.lat_accel_factor, native_factor * 1.3)
|
|
self.assertGreater(selected.LaC.pid.pos_limit, baseline.LaC.pid.pos_limit)
|
|
self.assertGreater(max(torque_differences), 1e-6)
|
|
feed(selected, now + 10_000_000, 230, active=False, enabled=False)
|
|
off, _ = selected.state_control()
|
|
self.assertFalse(off.latActive)
|
|
self.assertEqual(off.actuators.torque, 0.0)
|
|
self.assertEqual(selected.torque_host.applied, selected.torque_host.vehicle)
|
|
self.assertEqual(selected.LaC.pid.i, 0.0)
|
|
feed(selected, now + 20_000_000, 231, can_valid=False)
|
|
invalid, _ = selected.state_control()
|
|
self.assertFalse(invalid.latActive)
|
|
self.assertEqual(invalid.actuators.torque, 0.0)
|
|
feed(selected, now + 30_000_000, 232)
|
|
resumed, _ = selected.state_control()
|
|
self.assertTrue(resumed.latActive)
|
|
self.assertLess(abs(resumed.actuators.torque), selected.LaC.steer_max)
|
|
feed(selected, now + 40_000_000, 233, can_timeout=True)
|
|
timeout, _ = selected.state_control()
|
|
self.assertFalse(timeout.latActive)
|
|
self.assertEqual(timeout.actuators.torque, 0.0)
|
|
feed(selected, now + 50_000_000, 234, override=True)
|
|
prior_integral = selected.LaC.pid.i
|
|
override, _ = selected.state_control()
|
|
self.assertTrue(override.latActive)
|
|
self.assertTrue(math.isfinite(override.actuators.torque))
|
|
self.assertEqual(selected.LaC.pid.i, prior_integral)
|
|
|
|
def test_actual_controls_learned_to_user_to_stale_source(self):
|
|
with OpenpilotPrefix(), mock.patch.dict(os.environ, {'REPLAY': '1', 'TORQUE_REPLAY_RUNTIME': '1',
|
|
'AOL_REPLAY_RUNTIME': '0', 'LANE_CENTERING_REPLAY_RUNTIME': '0'}), \
|
|
mock.patch('openpilot.selfdrive.controls.controlsd.messaging.PubMaster'):
|
|
params = Params()
|
|
cp = publish_corolla_params(params)
|
|
Path(params.get_param_path(CONTROLLER_KEY)).write_bytes(replace_mode(None, cp, ControllerMode.STANDARD))
|
|
controls = Controls()
|
|
base = controls.torque_host.vehicle
|
|
target = base.lat_accel_factor * 1.1
|
|
for tick in range(120):
|
|
now = 1_000_000_000 + tick * 10_000_000
|
|
feed(controls, now, tick)
|
|
if tick % 25 == 0:
|
|
event = messaging.new_message('lateralTorqueParameters', valid=True, logMonoTime=now)
|
|
state = event.lateralTorqueParameters
|
|
state.useParams = state.valid = True
|
|
state.version = 1
|
|
state.latAccelFactorFiltered = target
|
|
state.latAccelOffsetFiltered = 0.1
|
|
state.frictionCoefficientFiltered = base.friction
|
|
controls.sm.update_msgs(now / 1e9, [event.as_reader()])
|
|
controls.state_control()
|
|
self.assertEqual(controls.torque_host.selected.source, TorqueSource.LEARNED)
|
|
self.assertGreater(controls.torque_host.applied.lat_accel_factor, base.lat_accel_factor)
|
|
params.put_bool('AdvancedLateralTune', True, block=True)
|
|
params.put('SteerLatAccel', base.lat_accel_factor * 1.2, block=True)
|
|
for tick in range(120, 230):
|
|
now = 1_000_000_000 + tick * 10_000_000
|
|
feed(controls, now, tick)
|
|
controls.state_control()
|
|
self.assertEqual(controls.torque_host.selected.source, TorqueSource.USER)
|
|
self.assertEqual(controls.torque_host.selected.lat_accel_offset, base.lat_accel_offset)
|
|
self.assertGreater(controls.torque_host.applied.lat_accel_factor, target)
|
|
params.put_bool('AdvancedLateralTune', False, block=True)
|
|
params.put('SteerLatAccel', base.lat_accel_factor, block=True)
|
|
for tick in range(230, 340):
|
|
now = 1_000_000_000 + tick * 10_000_000
|
|
feed(controls, now, tick)
|
|
controls.state_control()
|
|
self.assertEqual(controls.torque_host.selected, base)
|
|
self.assertAlmostEqual(controls.torque_host.applied.lat_accel_factor, base.lat_accel_factor)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|