Enable Global Driving Features

This commit is contained in:
firestar5683
2026-10-19 10:42:53 -05:00
parent 90dde04655
commit 7d145ee952
24 changed files with 780 additions and 110 deletions
@@ -34,14 +34,20 @@ class TestConditionalRuntimeLoop(unittest.TestCase):
with tempfile.TemporaryDirectory() as directory:
params = Params(directory)
params.put('CarParams', cp.to_bytes(), block=True)
params.put_bool('SpeedLimitController', False, block=True)
params.put_bool('ShowSpeedLimits', False, block=True)
params.put_bool('CurveSpeedController', False, block=True)
for enabled in (False, True):
params.put('ConditionalModeConfig', json.loads(encode_preferences(SavedPreferences(
mode=ModeChoice.CEM if enabled else ModeChoice.STOCK))), block=True)
params.put_bool('SafeMode', not enabled, block=True)
params.put_bool('UseStarPilotLongitudinalPlanner', enabled, block=True)
with self.subTest(enabled=enabled):
sm = Mock()
sm.update.side_effect = EndLoop
settings = {'CONDITIONAL_MODE_REPLAY_RUNTIME': '1' if enabled else '0',
settings = {'CONDITIONAL_MODE_REPLAY_RUNTIME': '0',
'SLC_REPLAY_RUNTIME': '0', 'CURVE_REPLAY_RUNTIME': '0', 'LONG_PLANNER_REPLAY_RUNTIME': '0'}
with patch.dict(os.environ, settings), patch.object(plannerd, 'Params', return_value=params), \
with patch.dict(os.environ, settings, clear=True), patch.object(plannerd, 'Params', return_value=params), \
patch.object(plannerd, 'config_realtime_process'), \
patch.object(plannerd, 'ConditionalPlannerHost', wraps=ConditionalPlannerHost) as conditional_ctor, \
patch.object(messaging, 'PubMaster') as publisher, \
@@ -52,9 +58,10 @@ class TestConditionalRuntimeLoop(unittest.TestCase):
call('carState', conflate=False),
call('slcAction', conflate=False),
call('slcCruiseEvent', conflate=False)])
# The typed manual-mode owner publishes status even without CEM.
# Status and transport are reserved independently of saved SLC/CEM
# admission, so a later saved change can attach without restarting.
self.assertEqual(publisher.call_args.args[0], ['longitudinalPlan', 'starpilotLongitudinalPlan',
'driverAssistance', 'slcState'])
'driverAssistance', 'slcState', 'slcCruiseCommand'])
self.assertEqual(conditional_ctor.call_count, int(enabled))
if enabled:
self.assertIs(conditional_ctor.call_args.args[0], params)
+1
View File
@@ -15,6 +15,7 @@ class AolVehiclePolicy:
distance_personality: bool = False
fixed_cruise_buttons: bool = False
lkas_button_supported: bool = True
auxiliary_lkas_supported: bool = False
physical_stalk_owner: bool = False
distance_pause_only: bool = False
paddle_pause: bool = False
@@ -12,6 +12,7 @@ class WheelSettingsPolicy:
paddle_pause: bool = True
runtime_supported: bool = False
lkas_button_supported: bool = False
auxiliary_lkas_supported: bool = False
def supports_ioniq6_configuration(cp) -> bool:
+4
View File
@@ -52,10 +52,14 @@ def qualified(cp, *, marked_only=False):
def policy_for(cp):
from openpilot.starpilot.controllers.wheel_actions import button_reported, eligible
admitted = qualified(cp)
auxiliary_lkas = bool(eligible(cp) and cp.brand == 'toyota' and button_reported(cp, 'LKASButtonControl'))
return AolVehiclePolicy(intent_supported=admitted, settings_supported=admitted, runtime_supported=admitted,
normal_runtime_supported=admitted, ordinary_axis_ack_required=qualified(cp, marked_only=True),
full_axis_runtime_required=admitted, fixed_cruise_buttons=admitted, distance_pause_only=admitted,
auxiliary_lkas_supported=auxiliary_lkas,
alternative_experience_addition=32 if admitted else 0)
@@ -0,0 +1,334 @@
"""Factory Toyota CAN evidence reaches existing auxiliary action owners."""
import json
from types import SimpleNamespace as NS
import pytest
from opendbc.can import CANPacker
from opendbc.car import Bus, structs
from opendbc.car.toyota.carstate import CarState
from opendbc.car.toyota.interface import CarInterface
from opendbc.car.toyota.values import CAR, DBC
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.starpilot.aol.intent import AolSettings
from openpilot.starpilot.aol.vehicle import policy_for
from openpilot.starpilot.car.toyota.aol import ToyotaCardIntent
from openpilot.starpilot.conditional_mode.card_input import conditional_manual_candidate
from openpilot.starpilot.conditional_mode.manual import ButtonTracker
from openpilot.starpilot.conditional_mode.policy import ModeChoice
from openpilot.starpilot.conditional_mode.preferences import SavedPreferences, encode_preferences
from openpilot.starpilot.conditional_mode.runtime_settings import ConditionalSettingsOwner
from openpilot.starpilot.controllers.coast import CoastRuntime
from openpilot.starpilot.controllers.tests import test_native_wheel_callbacks as native_callbacks
from openpilot.starpilot.controllers.tests.test_wheel_actions import Publisher, services, state, candidate, DRIVE, NOW
from openpilot.starpilot.controllers.wheel_actions import WheelConsumer, WheelPublisher
from openpilot.starpilot.ui.feature_settings_state import FeatureSettingsRequest
from openpilot.starpilot.ui.wheel_feature import WheelFeature
native_session = native_callbacks.native_session
KEY = 'LKASButtonControl'
TYPE = structs.CarState.ButtonEvent.Type
def factory(candidate=CAR.TOYOTA_RAV4_TSS2):
return CarInterface.get_params(candidate, {0: {}, 1: {}, 2: {}}, [], False, False, False)
class ToyotaButtons:
def __init__(self, cp):
self.state = CarState(cp)
self.parsers = CarState.get_can_parsers(cp)
self.packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
self.tick = 0
def sample(self, lda=0):
self.tick += 1
frame = self.packer.make_can_msg('LKAS_HUD', 2, {'LDA_ON_MESSAGE': lda})
self.parsers[Bus.cam].update([(self.tick * 10_000_000, [frame])])
result = self.state.update(self.parsers)
# Isolated edge bench: transport health is a synthetic premise here.
# The full CarInterface tests below verify real CAN validity without overrides.
result.canValid, result.canTimeout = True, False
return result
def emit(params, cp, code):
params.put(KEY, code, block=True)
source, parser = WheelPublisher(params), ToyotaButtons(cp)
source.observe(params, cp, parser.sample(), now_ns=NOW - 10_000_000, drive_id=DRIVE)
physical = parser.sample(1)
assert [(e.type, e.pressed) for e in physical.buttonEvents] == [(TYPE.lkas, True), (TYPE.lkas, False)]
commands = source.observe(params, cp, physical, now_ns=NOW, drive_id=DRIVE)
assert commands == ((KEY, code),)
assert not any(action for _, action in source.observe(params, cp, parser.sample(1), now_ns=NOW + 1, drive_id=DRIVE))
sender = Publisher()
source.publish(commands, cp, sender, now_ns=NOW, drive_id=DRIVE, source_car_ns=NOW, source_control_ns=NOW)
return messaging.log_from_bytes(sender.events[-1][1])
def feature(params, cp):
return WheelFeature(NS(params=params, vehicle_params=lambda: cp, configuration_vehicle=lambda: False,
configuration_longitudinal=lambda: cp.openpilotLongitudinalControl,
longitudinal_available=lambda: cp.openpilotLongitudinalControl, authority=lambda _: True))
def test_factory_settings_allow_auxiliary_lkas_hide_unreported_cancel_and_keep_ccmain_fixed(tmp_path):
params, cp = Params(str(tmp_path)), factory()
policy = policy_for(cp)
assert policy.fixed_cruise_buttons and policy.auxiliary_lkas_supported
wheel = feature(params, cp)
rows = {row.key: row for row in wheel.rows()}
row = rows['wheel:' + KEY]
assert {'Off', 'Bookmark', 'Traffic mode', 'Cycle personality', 'Experimental override'} <= set(row.choices)
assert not {'Pause steering', 'Pause longitudinal', 'Toggle AOL'} & set(row.choices)
assert not any('Cancel' in key or 'MainCruise' in key for key in rows)
assert wheel.apply(FeatureSettingsRequest(row.key, row.source, 'Bookmark', capability=row.capability))
assert params.get(KEY) == 8
assert not wheel.apply(FeatureSettingsRequest(row.key, b'8', 'Toggle AOL', capability=row.capability))
assert not wheel.apply(FeatureSettingsRequest('wheel:CancelButtonControl', None, 'Bookmark', capability=row.capability))
@pytest.mark.parametrize('code', (1, 5, 6, 8))
def test_factory_can_parser_to_publisher_to_consumer(tmp_path, code):
params, cp = Params(str(tmp_path)), factory()
event = emit(params, cp, code)
consumer = WheelConsumer()
command = consumer.accept(event, params, cp, services(cp), now_ns=NOW)
assert (command.key, command.action) == (KEY, code)
assert consumer.accept(event, params, cp, services(cp), now_ns=NOW) is None
@pytest.mark.parametrize('code', (3, 4, 9))
def test_axis_assignments_cannot_take_native_lkas_or_ccmain_ownership(tmp_path, code):
params, cp = Params(str(tmp_path)), factory()
params.put(KEY, code, block=True)
params.put('MainCruiseButtonControl', code, block=True)
native = ToyotaCardIntent(AolSettings(True, 0., code, code, (0, 0, 0), (0, 0, 0)))
assert native.settings.lkas_action == native.settings.main_action == 0
source, parser = WheelPublisher(params), ToyotaButtons(cp)
source.observe(params, cp, parser.sample(), now_ns=NOW - 10_000_000, drive_id=DRIVE)
assert not any(action for _, action in source.observe(params, cp, parser.sample(1), now_ns=NOW, drive_id=DRIVE))
@pytest.mark.parametrize('key', ('CancelButtonControl', 'LongCancelButtonControl', 'VeryLongCancelButtonControl'))
def test_unreported_toyota_button_receipts_are_rejected(tmp_path, key):
params, cp = Params(str(tmp_path)), factory()
code = 8
params.put(key, code, block=True)
sender, source = Publisher(), WheelPublisher(params)
source.publish(((key, code),), cp, sender, now_ns=NOW, drive_id=DRIVE, source_car_ns=NOW, source_control_ns=NOW)
event = messaging.log_from_bytes(sender.events[-1][1])
assert WheelConsumer().accept(event, params, cp, services(cp), now_ns=NOW) is None
source.observe(params, cp, state(), now_ns=NOW - 20_000_000, drive_id=DRIVE)
button = TYPE.cancel
source.observe(params, cp, state(True, button=button), now_ns=NOW - 10_000_000, drive_id=DRIVE)
assert not any(action for _, action in source.observe(params, cp, state(False, button=button), now_ns=NOW, drive_id=DRIVE))
def test_non_toyota_cancel_contract_preserved(tmp_path):
params = Params(str(tmp_path))
_, cp = candidate(False)
assert 'Bookmark' in feature(params, cp).choices('CancelButtonControl')
params.put('CancelButtonControl', 8, block=True)
source = WheelPublisher(params)
source.observe(params, cp, state(), now_ns=NOW - 20_000_000, drive_id=DRIVE)
source.observe(params, cp, state(True, button=TYPE.cancel), now_ns=NOW - 10_000_000, drive_id=DRIVE)
assert source.observe(params, cp, state(False, button=TYPE.cancel), now_ns=NOW, drive_id=DRIVE) == (('CancelButtonControl', 8),)
@pytest.mark.parametrize('code', (5, 8))
def test_factory_lkas_reaches_actual_native_action_callback(native_session, monkeypatch, code):
runtime, session, ui, _, _ = native_session
ui.CP = factory()
ui.has_longitudinal_control = ui.CP.openpilotLongitudinalControl
calls = []
session._native_favorite_actions = lambda: session.native_favorite_actions(
lambda: calls.append('bookmark') or True, lambda _: True, lambda: calls.append('experimental') or True)
queue = [emit(ui.params, ui.CP, code)]
monkeypatch.setattr(runtime.messaging, 'recv_one_or_none', lambda _: queue.pop(0) if queue else None)
session._poll_wheel(NOW)
session._poll_wheel(NOW)
assert calls == ['bookmark' if code == 8 else 'experimental']
def test_factory_lkas_traffic_reaches_existing_controller_owner(native_session):
_, _, ui, _, _ = native_session
ui.CP = factory()
event = emit(ui.params, ui.CP, 6)
runtime = CoastRuntime()
assert runtime.receive(event, ui.params, ui.CP, ui.sm, now_ns=NOW)
assert runtime.traffic_toggles == 1
assert not runtime.receive(event, ui.params, ui.CP, ui.sm, now_ns=NOW)
assert runtime.traffic_toggles == 1
def test_factory_lkas_cem_assignment_uses_existing_card_arbitration(tmp_path):
params, cp = Params(str(tmp_path)), factory()
params.put(KEY, 5, block=True)
params.put('ConditionalModeConfig', json.loads(encode_preferences(SavedPreferences(mode=ModeChoice.CEM))), block=True)
settings, tracker, parser = ConditionalSettingsOwner(params), ButtonTracker(), ToyotaButtons(cp)
sm = services(cp)
conditional_manual_candidate(parser.sample(), tracker, params, settings, cp, sm, now_ns=NOW - 10_000_000)
physical = parser.sample(1)
receipt = conditional_manual_candidate(physical, tracker, params, settings, cp, sm, now_ns=NOW)
assert receipt is not None and receipt[3] == ModeChoice.CEM
source = WheelPublisher(params)
source.observe(params, cp, state(), now_ns=NOW - 10_000_000, drive_id=DRIVE)
assert not any(action for _, action in source.observe(params, cp, physical, now_ns=NOW,
drive_id=DRIVE, blocked_keys={KEY}))
def test_factory_lkas_personality_reaches_native_saved_personality_callback(native_session, monkeypatch):
runtime, session, ui, _, _ = native_session
ui.CP = factory()
ui.has_longitudinal_control = ui.CP.openpilotLongitudinalControl
ui.params.put('LongitudinalPersonality', 1, block=True)
def save(value):
ui.params.put('LongitudinalPersonality', value, block=True)
return True
session._native_favorite_actions = lambda: session.native_favorite_actions(lambda: True, save, lambda: True)
queue = [emit(ui.params, ui.CP, 1)]
monkeypatch.setattr(runtime.messaging, 'recv_one_or_none', lambda _: queue.pop(0) if queue else None)
session._poll_wheel(NOW)
assert ui.params.get('LongitudinalPersonality') == 0
session._poll_wheel(NOW)
assert ui.params.get('LongitudinalPersonality') == 0
@pytest.mark.parametrize('case', ('invalid_can', 'stale_source', 'remapped', 'wrong_vehicle'))
def test_factory_lkas_consumer_rechecks_current_source_and_assignment(tmp_path, case):
params, cp = Params(str(tmp_path)), factory()
event = emit(params, cp, 8)
sm = services(cp)
if case == 'invalid_can':
sm['carState'].canValid = False
elif case == 'stale_source':
sm.recv_time['carState'] = (NOW - 300_000_000) / 1e9
elif case == 'remapped':
params.put(KEY, 5, block=True)
else:
cp = CarInterface.get_params(CAR.TOYOTA_COROLLA_TSS2, {0: {}, 1: {}, 2: {}}, [], False, False, False)
assert WheelConsumer().accept(event, params, cp, sm, now_ns=NOW) is None
class FactoryToyotaInterface:
"""Real CarInterface transport validity with complete factory parser messages."""
def __init__(self, cp, *, complete):
self.interface = CarInterface(cp)
self.interface.update([])
self.packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
self.tracked = {bus: tuple(parser.vl) for bus, parser in self.interface.can_parsers.items()}
self.complete = complete
def sample(self, now_ns, lda=0):
values = {'PCM_CRUISE': {'GAS_RELEASED': 1}, 'PCM_CRUISE_2': {'MAIN_ON': 1},
'GEAR_PACKET': {'GEAR': 0}, 'EPS_STATUS': {'LKA_STATE': 1},
'BODY_CONTROL_STATE': {}, 'LKAS_HUD': {'LDA_ON_MESSAGE': lda}}
frames = [self.packer.make_can_msg(name, 0 if bus == Bus.pt else 2, values.get(name, {}))
for bus, names in self.tracked.items() for name in names
if self.complete or bus == Bus.cam and name == 'LKAS_HUD']
return self.interface.update([(now_ns, frames)])
@pytest.mark.parametrize('complete', (True, False))
def test_actual_factory_interface_can_validity_gates_lkas_delivery(tmp_path, complete):
params, cp = Params(str(tmp_path)), factory()
params.put(KEY, 8, block=True)
bench = FactoryToyotaInterface(cp, complete=complete)
for tick in range(8):
physical = bench.sample(NOW - 90_000_000 + tick * 10_000_000)
assert physical.canValid == complete
if complete:
assert not physical.canTimeout
assert physical.cruiseState.available
assert physical.gearShifter == structs.CarState.GearShifter.drive
assert not (physical.doorOpen or physical.seatbeltUnlatched or physical.steerFaultPermanent)
source = WheelPublisher(params)
neutral = bench.sample(NOW - 10_000_000)
source.observe(params, cp, neutral, now_ns=NOW - 10_000_000, drive_id=DRIVE)
physical = bench.sample(NOW, lda=1)
assert physical.canValid == complete # Never override parser or interface health.
assert [(e.type, e.pressed) for e in physical.buttonEvents] == [(TYPE.lkas, True), (TYPE.lkas, False)]
commands = source.observe(params, cp, physical, now_ns=NOW, drive_id=DRIVE)
assert commands == (((KEY, 8),) if complete else ())
sender = Publisher()
# For missing CAN, even a forged publication cannot pass current source health.
source.publish(commands if complete else ((KEY, 8),), cp, sender, now_ns=NOW, drive_id=DRIVE,
source_car_ns=NOW, source_control_ns=NOW)
sm = services(cp)
sm['carState'] = physical
accepted = WheelConsumer().accept(messaging.log_from_bytes(sender.events[-1][1]), params, cp, sm, now_ns=NOW)
if complete:
assert (accepted.key, accepted.action) == (KEY, 8)
else:
assert accepted is None
@pytest.mark.parametrize('vehicle', (CAR.TOYOTA_SIENNA_4TH_GEN, CAR.TOYOTA_RAV4_TSS2_2023))
def test_parser_supported_toyota_auxiliary_lkas_does_not_require_aol_admission(tmp_path, vehicle):
params, cp = Params(str(tmp_path)), factory(vehicle)
baseline = cp.to_dict()
policy = policy_for(cp)
assert not (policy.intent_supported or policy.settings_supported or policy.runtime_supported or
policy.normal_runtime_supported or policy.full_axis_runtime_required)
assert policy.alternative_experience_addition == policy.safety_param_addition == 0
assert policy.auxiliary_lkas_supported
wheel = feature(params, cp)
rows = {row.key: row for row in wheel.rows()}
row = rows['wheel:' + KEY]
assert 'Bookmark' in row.choices
assert not {'Pause steering', 'Pause longitudinal', 'Toggle AOL'} & set(row.choices)
assert not any('MainCruise' in key or 'Cancel' in key for key in rows)
assert wheel.apply(FeatureSettingsRequest(row.key, row.source, 'Bookmark', capability=row.capability))
assert not wheel.apply(FeatureSettingsRequest(row.key, b'8', 'Toggle AOL', capability=row.capability))
# This bench uses full factory parser transport, without forcing CAN validity.
bench = FactoryToyotaInterface(cp, complete=True)
for tick in range(8):
physical = bench.sample(NOW - 90_000_000 + tick * 10_000_000)
assert physical.canValid and not physical.canTimeout
source = WheelPublisher(params)
source.observe(params, cp, bench.sample(NOW - 10_000_000), now_ns=NOW - 10_000_000, drive_id=DRIVE)
physical = bench.sample(NOW, lda=1)
assert physical.canValid and not physical.canTimeout
assert [(e.type, e.pressed) for e in physical.buttonEvents] == [(TYPE.lkas, True), (TYPE.lkas, False)]
commands = source.observe(params, cp, physical, now_ns=NOW, drive_id=DRIVE)
assert commands == ((KEY, 8),)
sender = Publisher()
source.publish(commands, cp, sender, now_ns=NOW, drive_id=DRIVE, source_car_ns=NOW, source_control_ns=NOW)
sm = services(cp)
sm['carState'] = physical
accepted = WheelConsumer().accept(messaging.log_from_bytes(sender.events[-1][1]), params, cp, sm, now_ns=NOW)
assert (accepted.key, accepted.action) == (KEY, 8)
assert cp.to_dict() == baseline # Auxiliary settings grant no CP/native authority.
@pytest.mark.parametrize('vehicle', (CAR.TOYOTA_SIENNA_4TH_GEN, CAR.TOYOTA_RAV4_TSS2_2023))
def test_aol_unsupported_toyota_bookmark_reaches_real_native_callback(native_session, monkeypatch, vehicle):
runtime, session, ui, _, _ = native_session
ui.CP = factory(vehicle)
ui.has_longitudinal_control = ui.CP.openpilotLongitudinalControl
assert not policy_for(ui.CP).intent_supported
calls = []
session._native_favorite_actions = lambda: session.native_favorite_actions(
lambda: calls.append('bookmark') or True, lambda _: True, lambda: True)
queue = [emit(ui.params, ui.CP, 8)]
monkeypatch.setattr(runtime.messaging, 'recv_one_or_none', lambda _: queue.pop(0) if queue else None)
session._poll_wheel(NOW)
session._poll_wheel(NOW)
assert calls == ['bookmark']
@pytest.mark.parametrize('field', ('notCar', 'passive', 'dashcamOnly'))
def test_toyota_auxiliary_lkas_keeps_existing_eligibility_limits(tmp_path, field):
cp = factory(CAR.TOYOTA_SIENNA_4TH_GEN)
setattr(cp, field, True)
assert not policy_for(cp).auxiliary_lkas_supported
assert feature(Params(str(tmp_path)), cp).rows() == []
def test_toyota_without_parser_lkas_source_has_no_auxiliary_row(tmp_path):
cp = factory(CAR.TOYOTA_SIENNA)
assert not policy_for(cp).auxiliary_lkas_supported
assert feature(Params(str(tmp_path)), cp).choices(KEY) == ()
@@ -35,6 +35,19 @@ def eligible(cp):
return False
def button_reported(cp, key):
"""Match known parser omissions without changing other ports' button contracts."""
if cp.brand == 'toyota':
# Toyota CarState emits LKAS and distance events, never Cancel.
if key in KEYS[5:8]:
return False
if key == 'LKASButtonControl':
from opendbc.car.toyota.values import CAR, ToyotaFlags
return bool(cp.flags & (ToyotaFlags.TSS2 | ToyotaFlags.LONG_FILTER) or
cp.carFingerprint == CAR.TOYOTA_PRIUS_RETROFIT)
return True
def cp_fingerprint(cp):
reader = cp.as_reader() if hasattr(cp, "as_reader") else cp
return hashlib.sha256(reader.as_builder().to_bytes()).hexdigest()
@@ -154,7 +167,7 @@ class WheelPublisher:
if index < len(keys):
gestures.append((keys[index], gesture))
commands = [(key, mapping[key]) for key, _ in gestures if key is not None and key not in blocked_keys and
key not in self.axis_keys and mapping[key] in DELIVERED_ACTIONS and
key not in self.axis_keys and button_reported(cp, key) and mapping[key] in DELIVERED_ACTIONS and
(key != "MainCruiseButtonControl" or mapping[key] == 10)]
if commands and not audit:
current = capture(params)
@@ -231,7 +244,7 @@ class WheelConsumer:
if action == 0:
if key:
return None
elif key not in KEYS or action not in DELIVERED_ACTIONS:
elif key not in KEYS or action not in DELIVERED_ACTIONS or not button_reported(cp, key):
return None
else:
mapping = capture(params)
+1 -1
View File
@@ -46,7 +46,7 @@ def frame_from_submaster(sm, CP, now_ns: int, *, drive_id: int = 0) -> Frame:
car_state, car_control = sm['carState'], sm['carControl']
controls, selfdrive = sm['controlsState'], sm['selfdriveState']
cruise_mps = float(car_state.vCruise) / 3.6
system_longitudinal = bool(CP.openpilotLongitudinalControl) and not bool(CP.pcmCruise)
system_longitudinal = bool(CP.openpilotLongitudinalControl) and not any(getattr(CP, flag, False) for flag in ('passive', 'dashcamOnly', 'notCar'))
return Frame(now_ns=now_ns, model_ns=model_ns, inputs_fresh=current and bool(car_state.canValid) and not bool(car_state.canTimeout),
profile=profile, system_longitudinal=system_longitudinal, long_active=bool(car_control.longActive),
enabled=bool(selfdrive.enabled), force_decel=bool(controls.forceDecel), cruise_mps=cruise_mps,
@@ -107,7 +107,10 @@ class TestCurveStatus(unittest.TestCase):
with self.subTest(slc=slc_on, curve=curve_on), tempfile.TemporaryDirectory() as directory, ExitStack() as stack:
params = Params(directory)
params.put('CarParams', cp_bytes, block=True)
params.put_bool(MASTER_KEY, True, block=True)
# Saved requests, not a replay flag set to zero, choose the hosts.
params.put_bool(MASTER_KEY, curve_on, block=True)
params.put_bool('SpeedLimitController', slc_on, block=True)
params.put_bool('ShowSpeedLimits', slc_on, block=True)
sm = bus(NOW)
for name in ('deviceState', 'slcDashboardObservation'):
sm.messages[name] = getattr(messaging.new_message(name), name)
@@ -161,7 +164,7 @@ class TestCurveStatus(unittest.TestCase):
plannerd.main()
counts = Counter(name for name, _raw in sent)
self.assertEqual(counts['longitudinalPlan'], 2)
self.assertEqual(counts['slcState'], 2) # Display-only signs still publish with both controllers off.
self.assertEqual(counts['slcState'], 2) # Shared optional status still publishes with both controllers off.
self.assertEqual(sum(name == 'slcCruiseEvent' for name, _kw in subscriptions), 1)
self.assertTrue(all(not kw['conflate'] for _name, kw in subscriptions))
for name, raw in sent:
@@ -0,0 +1,16 @@
"""Generic optional-feature admission from finalized native CarParams.
Admission creates a host; the host still checks live axis and source authority.
PCM cruise describes engagement/set-speed integration, not who controls acceleration.
"""
def display_supported(cp) -> bool:
return bool(cp is not None and getattr(cp, 'carFingerprint', '') and
getattr(cp, 'passive', None) is False and
getattr(cp, 'dashcamOnly', None) is False and
getattr(cp, 'notCar', None) is False)
def longitudinal_supported(cp) -> bool:
return display_supported(cp) and getattr(cp, 'openpilotLongitudinalControl', None) is True
+7 -9
View File
@@ -7,12 +7,12 @@ each owner still verifies current vehicle, source, clock, and axis authority.
from collections.abc import Mapping
from dataclasses import replace
from opendbc.car.gm.feature_capabilities import longitudinal_supported as gm_long_supported, display_supported as gm_display_supported
from opendbc.car.gm.feature_capabilities import longitudinal_supported as gm_long_supported
from openpilot.starpilot.conditional_mode.preferences import PreferenceError, decode_preferences
from openpilot.starpilot.aol.intent import independent_axis_requested, read_settings as read_aol_settings
from openpilot.starpilot.aol.vehicle import policy_for as aol_policy_for
from openpilot.starpilot.longitudinal.ioniq6_start import eligible as ioniq6_long_eligible
from openpilot.starpilot.feature_capabilities import display_supported, longitudinal_supported
from openpilot.starpilot.longitudinal.profile_runtime import read_settings as read_profile_settings, selected_profiles_requested, read_global_powertrain_preset
from openpilot.starpilot.longitudinal.profile_preferences import read_document_value
from openpilot.starpilot.saved_source import read_saved
@@ -56,7 +56,7 @@ def requested(params, feature: str, *, include_auxiliary: bool = False, saved_sl
if feature in ('slc', 'vision'):
settings = read_slc_settings(params) if saved_slc is None else saved_slc
return (settings.enabled or settings.display) if feature == 'slc' else (
settings.display and Source.VISION in settings.selection.slots)
(settings.enabled or settings.display) and Source.VISION in settings.selection.slots)
raise ValueError('unknown optional feature')
@@ -78,9 +78,7 @@ def enabled(params, cp, feature: str, environment: Mapping[str, str], *, saved_s
return True
if feature != 'vision' and environment.get(flags[feature]) == '1':
return True
capable = cp is not None and (ioniq6_long_eligible(cp) or
(feature in ('conditional', 'curve', 'slc', 'profile') and gm_long_supported(cp)) or
(feature in ('slc', 'vision') and gm_display_supported(cp)))
capable = display_supported(cp) if feature in ('slc', 'vision') else longitudinal_supported(cp)
powertrain_requested = (feature == 'profile' and cp is not None and gm_long_supported(cp) and
read_document_value(params).valid and read_global_powertrain_preset(params, cp) is not None)
return bool(capable and (requested(params, feature, saved_slc=saved_slc) or powertrain_requested))
@@ -89,13 +87,13 @@ def enabled(params, cp, feature: str, environment: Mapping[str, str], *, saved_s
def slc_transport_capable(cp, environment: Mapping[str, str]) -> bool:
"""Reserve optional transport without admitting a saved request or control."""
return bool(environment.get('SLC_REPLAY_RUNTIME') == '1' or
(cp is not None and (ioniq6_long_eligible(cp) or gm_long_supported(cp) or gm_display_supported(cp))))
display_supported(cp))
def slc_runtime_settings(params, cp, environment: Mapping[str, str], *, saved_slc: SlcSettings | None = None):
"""Stock display startup cannot inherit a saved speed-control request."""
settings = read_slc_settings(params) if saved_slc is None else saved_slc
if environment.get('SLC_REPLAY_RUNTIME') != '1' and gm_display_supported(cp) and not gm_long_supported(cp):
if environment.get('SLC_REPLAY_RUNTIME') != '1' and not longitudinal_supported(cp):
return replace(settings, enabled=False, display=settings.display or settings.enabled, acceptance=replace(settings.acceptance, display_only=True))
return settings
@@ -103,5 +101,5 @@ def slc_runtime_settings(params, cp, environment: Mapping[str, str], *, saved_sl
def vision_control_enabled(params, cp, *, saved_slc: SlcSettings | None = None) -> bool:
"""Source selection never turns stock-cruise or display-only sessions into control."""
settings = read_slc_settings(params) if saved_slc is None else saved_slc
return bool(cp is not None and (ioniq6_long_eligible(cp) or gm_long_supported(cp)) and
return bool(longitudinal_supported(cp) and
settings.enabled and Source.VISION in settings.selection.slots)
+1 -1
View File
@@ -105,7 +105,7 @@ def _qualified(ctx: AuthorityContext, group: str) -> bool:
return bool((cp.openpilotLongitudinalControl or ctx.configuration_longitudinal) and
not cp.notCar and not cp.dashcamOnly and not cp.passive)
if group == "long":
return bool(((cp.openpilotLongitudinalControl and not cp.pcmCruise) or ctx.configuration_longitudinal) and
return bool((cp.openpilotLongitudinalControl or ctx.configuration_longitudinal) and
not cp.notCar and not cp.dashcamOnly and not cp.passive)
if group in ("aol", "aol_wheel") and ctx.configuration_vehicle and ctx.parked:
from openpilot.starpilot.aol.vehicle import configuration_settings_policy
@@ -432,7 +432,7 @@ class SettingsGatewayTest(unittest.TestCase):
vehicle = HondaInterface.get_params(HondaCAR.HONDA_CIVIC_BOSCH, gen_empty_fingerprint(), [], True, False, False)
self.assertTrue(vehicle.openpilotLongitudinalControl and not vehicle.pcmCruise)
self.assertFalse(diagnostic_choice_enabled({}, vehicle))
self.assertTrue(diagnostic_choice_enabled({}, vehicle))
self.context.value = AuthorityContext(True, vehicle, vehicle.to_bytes())
self.params.put('SLCPriority1', 'Dashboard', block=True)
self.params.put('SLCPriority2', 'Dashboard', block=True)
@@ -444,22 +444,56 @@ class SettingsGatewayTest(unittest.TestCase):
rows = self.page("slc")["rows"]
primary = next(row for row in rows if row["label"] == "Primary source")
secondary = next(row for row in rows if row["label"] == "Secondary source")
self.assertEqual("Vision" in primary["choices"], available)
self.assertEqual("Vision" in secondary["choices"], available)
self.assertIn("Vision", primary["choices"])
self.assertIn("Vision", secondary["choices"])
self.assertNotIn("require your confirmation", primary["reason"])
confirmation = next(row for row in rows if row["label"] == "Require confirmation")
self.assertEqual(confirmation["value"], "Off")
self.assertIn("first speed limit", confirmation["reason"])
self.assertIn("any source", confirmation["reason"])
with mock.patch.dict(os.environ, {"SLC_REPLAY_RUNTIME": "1", "SLC_VISION_DEVELOPMENT": "1"}):
with mock.patch.dict(os.environ, {"SLC_REPLAY_RUNTIME": "0", "SLC_VISION_DEVELOPMENT": "0"}):
# A normal finalized car offers Vision without development/replay flags.
page = self.page("slc")
index = next(i for i, row in enumerate(page["rows"]) if row["label"] == "Primary source")
intent = self.gateway.preview(page["view"], index, 1, self.session, self.generation)
self.assertEqual(intent["proposed"], "Vision")
with mock.patch.dict(os.environ, {"SLC_VISION_DEVELOPMENT": "0"}):
self.assertFalse(self.gateway.confirm(intent["intent"], self.session, self.generation))
self.assertEqual(read_saved(self.params, "SLCPriority1", 128)[0], b"Dashboard")
self.assertTrue(self.gateway.confirm(intent["intent"], self.session, self.generation))
self.assertEqual(read_saved(self.params, "SLCPriority1", 128)[0], b"Vision")
self.params.put('SLCPriority1', 'Dashboard', block=True)
original_context = self.context.value
for withdrawal in ('passive', 'unavailable'):
with self.subTest(withdrawal=withdrawal):
self.context.value = original_context
page = self.page("slc")
index = next(i for i, row in enumerate(page["rows"]) if row["label"] == "Primary source")
intent = self.gateway.preview(page["view"], index, 1, self.session, self.generation)
self.assertEqual(intent["proposed"], "Vision")
if withdrawal == 'passive':
withdrawn = vehicle.as_reader().as_builder()
withdrawn.passive = True
self.context.value = AuthorityContext(True, withdrawn, withdrawn.to_bytes())
else:
self.context.value = AuthorityContext(True, None, None)
with self.assertRaises(SettingsChanged):
self.gateway.confirm(intent["intent"], self.session, self.generation)
self.assertEqual(read_saved(self.params, "SLCPriority1", 128)[0], b"Dashboard")
# Source choice is display access on a native stock-ACC car, never a
# grant of longitudinal control from a remembered controller switch.
from openpilot.starpilot.feature_runtime import slc_runtime_settings, vision_control_enabled
stock = HondaInterface.get_params(HondaCAR.HONDA_CIVIC_BOSCH, gen_empty_fingerprint(), [], False, False, False)
self.assertFalse(stock.openpilotLongitudinalControl)
self.context.value = AuthorityContext(True, stock, stock.to_bytes())
self.params.put_bool('SpeedLimitController', True, block=True)
page = self.page("slc")
index = next(i for i, row in enumerate(page["rows"]) if row["label"] == "Primary source")
intent = self.gateway.preview(page["view"], index, 1, self.session, self.generation)
self.assertEqual(intent["proposed"], "Vision")
self.assertTrue(self.gateway.confirm(intent["intent"], self.session, self.generation))
settings = slc_runtime_settings(self.params, stock, {})
self.assertTrue(settings.display and settings.acceptance.display_only)
self.assertFalse(settings.enabled or vision_control_enabled(self.params, stock))
def test_pip_saved_choices_no_vehicle_cp_and_one_use_confirmation(self):
from openpilot.starpilot.ui.pip_preferences import BLINKER, MASK, read_pip
@@ -488,19 +488,22 @@ class ProfileRuntimeTests(unittest.TestCase):
tuple(message.longitudinalPlan.speeds)))
self.params.put('CarParams', self.cp.to_bytes(), block=True)
self.params.put_bool('CustomPersonalities', True, block=True)
self.params.put('StandardFollow', 2.5, block=True)
self.params.put('StandardFollowHigh', 2.5, block=True)
results = []
for enabled in (False, True):
self.params.put_bool('CustomPersonalities', enabled, block=True)
# Explicit native response removes the separate default Comfort request;
# this comparison isolates the saved following/jerk master switch.
self.params.put('LongitudinalPersonalityProfiles', current_profile_document(
default_personality_profiles(False), enabled=enabled,
selected_deceleration_profile='dom_default'), block=True)
data, _ = messages(lead=True)
data['carState'].canValid = True
data['carControl'].longActive = True
sm = LoopSM(data)
publisher = Publisher()
environment = {}
if enabled:
environment['LONG_PLANNER_REPLAY_RUNTIME'] = '1'
environment = {} # Normal admission follows the actual saved request.
with (mock.patch.object(plannerd, 'Params', return_value=self.params),
mock.patch.object(plannerd, 'config_realtime_process'),
mock.patch.object(plannerd, 'LeadApproachPreferences', return_value=None),
@@ -227,7 +227,12 @@ def test_cached_provider_verification_invalidates_on_replacement_and_source_addi
os.utime(binary, ns=(prior.st_atime_ns, prior.st_mtime_ns + 1_000_000_000))
assert binary.stat().st_mtime_ns != prior.st_mtime_ns
assert ready() and len(calls) == 3
prior_source = source.stat()
(source / 'new.go').write_bytes(b'package main')
# Directory additions can share a metadata timestamp tick too. The cache
# watches directory identity before enumerating newly added source files.
os.utime(source, ns=(prior_source.st_atime_ns, prior_source.st_mtime_ns + 1_000_000_000))
assert source.stat().st_mtime_ns != prior_source.st_mtime_ns
assert not ready() and len(calls) == 4
@@ -1,11 +1,10 @@
"""Availability of the optional camera speed-limit source."""
from collections.abc import Mapping
from openpilot.starpilot.car.hyundai.aol import ioniq6_settings_capable
from opendbc.car.gm.feature_capabilities import display_supported as gm_display_supported
from openpilot.starpilot.feature_capabilities import display_supported
def diagnostic_choice_enabled(environment: Mapping[str, str], cp=None) -> bool:
# A saved source choice does not itself authorize Vision speed control.
return ((environment.get("SLC_REPLAY_RUNTIME") == "1" and environment.get("SLC_VISION_DEVELOPMENT") == "1") or
(cp is not None and (ioniq6_settings_capable(cp) or gm_display_supported(cp))))
display_supported(cp))
@@ -1,9 +1,8 @@
"""Saved Ioniq runtime startup does not replace live driving authority."""
"""Saved generic runtime startup does not replace live driving authority."""
from pathlib import Path
import json
from tempfile import TemporaryDirectory
from unittest.mock import patch
from openpilot.starpilot.conditional_mode.policy import ModeChoice
from openpilot.starpilot.conditional_mode.preferences import SavedPreferences, encode_preferences
@@ -12,6 +11,22 @@ from openpilot.starpilot.longitudinal.profile_document import default_personalit
from openpilot.starpilot.speed_limits.vision_gate import diagnostic_choice_enabled
def long_cp():
from opendbc.car.toyota.interface import CarInterface
from opendbc.car.toyota.values import CAR
cp = CarInterface.get_non_essential_params(CAR.TOYOTA_RAV4_TSS2)
assert cp.openpilotLongitudinalControl and cp.pcmCruise
return cp
def stock_cp():
from opendbc.car.honda.interface import CarInterface
from opendbc.car.honda.values import CAR
cp = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC_BOSCH)
assert not cp.openpilotLongitudinalControl
return cp
class Files:
def __init__(self, root: Path):
self.root = root
@@ -24,83 +39,79 @@ class Files:
return b'0' if key == 'CustomPersonalities' else {}
def test_readable_absence_starts_factory_cem_only_on_exact_eligible_cp():
def test_readable_absence_starts_factory_cem_only_on_native_long_cp():
with TemporaryDirectory() as temporary:
params = Files(Path(temporary))
with patch('openpilot.starpilot.feature_runtime.ioniq6_long_eligible', side_effect=lambda cp: cp == 'tagged'):
assert requested(params, 'conditional')
assert enabled(params, 'tagged', 'conditional', {})
assert not enabled(params, 'other', 'conditional', {})
for name in ('curve', 'slc', 'vision', 'profile'):
assert not enabled(params, object(), name, {})
(params.root / 'SafeMode').write_bytes(b'1')
assert not requested(params, 'conditional')
(params.root / 'SafeMode').write_bytes(b'0')
(params.root / 'ConditionalModeConfig').write_bytes(b'{broken')
(params.root / 'CurveSpeedController').write_bytes(b'true')
assert not requested(params, 'conditional')
assert not requested(params, 'curve')
(params.root / 'ConditionalModeConfig').unlink()
(params.root / 'ConditionalModeConfig').mkdir()
assert not requested(params, 'conditional') # Read error is never factory absence.
assert requested(params, 'conditional')
assert enabled(params, long_cp(), 'conditional', {})
assert not enabled(params, stock_cp(), 'conditional', {})
for name in ('curve', 'slc', 'vision', 'profile'):
assert not enabled(params, object(), name, {})
(params.root / 'SafeMode').write_bytes(b'1')
assert not requested(params, 'conditional')
(params.root / 'SafeMode').write_bytes(b'0')
(params.root / 'ConditionalModeConfig').write_bytes(b'{broken')
(params.root / 'CurveSpeedController').write_bytes(b'true')
assert not requested(params, 'conditional')
assert not requested(params, 'curve')
(params.root / 'ConditionalModeConfig').unlink()
(params.root / 'ConditionalModeConfig').mkdir()
assert not requested(params, 'conditional') # Read error is never factory absence.
def test_exact_tagged_cp_and_saved_document_start_owner_without_granting_other_cars():
def test_native_long_cp_and_saved_document_start_owner_without_granting_stock_control():
with TemporaryDirectory() as temporary:
params = Files(Path(temporary))
(params.root / 'ConditionalModeConfig').write_bytes(encode_preferences(SavedPreferences(mode=ModeChoice.STOCK)))
(params.root / 'CurveSpeedController').write_bytes(b'1')
with patch('openpilot.starpilot.feature_runtime.ioniq6_long_eligible', side_effect=lambda cp: cp == 'tagged'):
for name in ('conditional', 'curve'):
assert enabled(params, 'tagged', name, {})
assert not enabled(params, 'stock', name, {})
(params.root / 'SafeMode').write_bytes(b'1')
assert not enabled(params, 'tagged', 'conditional', {})
for name in ('conditional', 'curve'):
assert enabled(params, long_cp(), name, {})
assert not enabled(params, stock_cp(), name, {})
(params.root / 'SafeMode').write_bytes(b'1')
assert not enabled(params, long_cp(), 'conditional', {})
def test_global_braking_choice_starts_only_qualified_profile_owner_without_custom_master():
with TemporaryDirectory() as temporary:
params = Files(Path(temporary))
source = params.root / 'LongitudinalPersonalityProfiles'
with patch('openpilot.starpilot.feature_runtime.ioniq6_long_eligible', side_effect=lambda cp: cp == 'tagged'):
assert requested(params, 'profile')
assert enabled(params, 'tagged', 'profile', {})
assert not enabled(params, 'stock', 'profile', {})
for response in ('eco', 'sport'):
source.write_text(json.dumps(profile_document(default_personality_profiles(False), enabled=False,
global_braking_response=response)))
assert requested(params, 'profile')
assert enabled(params, 'tagged', 'profile', {})
assert not enabled(params, 'stock', 'profile', {})
assert requested(params, 'profile')
assert enabled(params, long_cp(), 'profile', {})
assert not enabled(params, stock_cp(), 'profile', {})
for response in ('eco', 'sport'):
source.write_text(json.dumps(profile_document(default_personality_profiles(False), enabled=False,
selected_deceleration_profile='dom_default')))
assert not requested(params, 'profile')
bad = profile_document(default_personality_profiles(False), enabled=False)
bad['globalBrakingResponse'] = 'unsupported'
source.write_text(json.dumps(bad))
assert not requested(params, 'profile')
source.write_text('{invalid')
assert not requested(params, 'profile')
(params.root / 'SafeMode').write_bytes(b'corrupt')
assert not enabled(params, 'tagged', 'conditional', {})
global_braking_response=response)))
assert requested(params, 'profile')
assert enabled(params, long_cp(), 'profile', {})
assert not enabled(params, stock_cp(), 'profile', {})
source.write_text(json.dumps(profile_document(default_personality_profiles(False), enabled=False,
selected_deceleration_profile='dom_default')))
assert not requested(params, 'profile')
bad = profile_document(default_personality_profiles(False), enabled=False)
bad['globalBrakingResponse'] = 'unsupported'
source.write_text(json.dumps(bad))
assert not requested(params, 'profile')
source.write_text('{invalid')
assert not requested(params, 'profile')
(params.root / 'SafeMode').write_bytes(b'corrupt')
assert not enabled(params, long_cp(), 'conditional', {})
def test_vision_control_needs_saved_master_and_vision_source_on_exact_long_cp():
def test_vision_control_needs_saved_master_and_vision_source_on_native_long_cp():
with TemporaryDirectory() as temporary:
params = Files(Path(temporary))
(params.root / 'SpeedLimitController').write_bytes(b'1')
(params.root / 'SLCPriority1').write_bytes(b'Dashboard')
(params.root / 'SLCPriority2').write_bytes(b'Vision')
with patch('openpilot.starpilot.feature_runtime.ioniq6_long_eligible', side_effect=lambda cp: cp == 'tagged'):
from openpilot.starpilot.feature_runtime import vision_control_enabled
assert requested(params, 'vision')
assert enabled(params, 'tagged', 'vision', {})
assert not enabled(params, 'stock', 'vision', {})
assert vision_control_enabled(params, 'tagged')
assert not vision_control_enabled(params, 'stock')
(params.root / 'SpeedLimitController').write_bytes(b'0')
assert enabled(params, 'tagged', 'vision', {}) # Default-on display still needs its selected Vision producer.
assert not vision_control_enabled(params, 'tagged')
from openpilot.starpilot.feature_runtime import vision_control_enabled
assert requested(params, 'vision')
assert enabled(params, long_cp(), 'vision', {})
assert enabled(params, stock_cp(), 'vision', {}) # Stock owner may display selected Vision.
assert vision_control_enabled(params, long_cp())
assert not vision_control_enabled(params, stock_cp())
(params.root / 'SpeedLimitController').write_bytes(b'0')
assert enabled(params, long_cp(), 'vision', {}) # Default-on display still needs its selected Vision producer.
assert not vision_control_enabled(params, long_cp())
def test_explicit_replay_flags_remain_available_for_existing_replay_fixtures():
@@ -113,13 +124,11 @@ def test_explicit_replay_flags_remain_available_for_existing_replay_fixtures():
assert not enabled(params, None, 'vision', {'SLC_VISION_DEVELOPMENT': '1'})
def test_saved_vision_choice_is_offered_only_for_supported_vehicle_or_explicit_replay():
with patch('openpilot.starpilot.speed_limits.vision_gate.ioniq6_settings_capable',
side_effect=lambda cp: cp == 'reviewed_ioniq'):
assert diagnostic_choice_enabled({}, 'reviewed_ioniq')
assert not diagnostic_choice_enabled({}, 'stock_or_other')
assert not diagnostic_choice_enabled({})
assert diagnostic_choice_enabled({'SLC_REPLAY_RUNTIME': '1', 'SLC_VISION_DEVELOPMENT': '1'})
def test_saved_vision_choice_is_offered_for_native_display_vehicle_or_explicit_replay():
assert diagnostic_choice_enabled({}, long_cp())
assert diagnostic_choice_enabled({}, stock_cp()) # Source visibility grants no speed authority.
assert not diagnostic_choice_enabled({})
assert diagnostic_choice_enabled({'SLC_REPLAY_RUNTIME': '1', 'SLC_VISION_DEVELOPMENT': '1'})
def test_gm_saved_profiles_require_existing_longitudinal_owner():
@@ -0,0 +1,226 @@
"""Generic admission reaches real Toyota hosts and native planner application."""
from types import SimpleNamespace as NS
from unittest.mock import Mock, patch
import pytest
from opendbc.car.toyota.interface import CarInterface
from opendbc.car.toyota.values import CAR
from openpilot.common.params import Params
from openpilot.selfdrive.controls import plannerd
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner
from openpilot.selfdrive.controls.tests.test_curve_runtime_loop import bus, NOW
from openpilot.starpilot.curve_speed.host import CurveHost
from openpilot.starpilot.feature_runtime import enabled, slc_runtime_settings, vision_control_enabled
from openpilot.starpilot.longitudinal.cruise_ceiling import CruiseCeiling
from openpilot.starpilot.longitudinal.profile_document import default_personality_profiles, profile_document
from openpilot.starpilot.longitudinal.profile_runtime import ProfileHost
from openpilot.starpilot.speed_limits.acceptance import Authority, LongitudinalOwner, Mode
from openpilot.starpilot.speed_limits.selection import Source
from openpilot.starpilot.speed_limits.vision_gate import diagnostic_choice_enabled
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
def toyota():
cp = CarInterface.get_non_essential_params(CAR.TOYOTA_RAV4_TSS2)
assert cp.openpilotLongitudinalControl and cp.pcmCruise
return cp
@pytest.mark.parametrize('brand', ['toyota', 'honda', 'hyundai', 'subaru', 'ford', 'volkswagen', 'mazda', 'nissan', 'tesla', 'rivian', 'future'])
def test_generic_admission_is_brand_independent_and_stock_is_display_only(tmp_path, brand):
params = Params(str(tmp_path))
params.put_bool('CurveSpeedController', True, block=True)
params.put_bool('SpeedLimitController', True, block=True)
cp = NS(brand=brand, carFingerprint=brand + '_native', openpilotLongitudinalControl=True,
pcmCruise=True, passive=False, dashcamOnly=False, notCar=False)
for feature in ('conditional', 'curve', 'profile', 'slc', 'vision'):
assert enabled(params, cp, feature, {})
assert diagnostic_choice_enabled({}, cp)
assert vision_control_enabled(params, cp)
cp.openpilotLongitudinalControl = False
for feature in ('conditional', 'curve', 'profile'):
assert not enabled(params, cp, feature, {})
settings = slc_runtime_settings(params, cp, {})
assert settings.display and not settings.enabled and settings.acceptance.display_only
assert enabled(params, cp, 'slc', {}) and enabled(params, cp, 'vision', {})
assert not vision_control_enabled(params, cp)
for field in ('passive', 'dashcamOnly', 'notCar'):
setattr(cp, field, True)
for feature in ('conditional', 'curve', 'profile', 'slc', 'vision'):
assert not enabled(params, cp, feature, {})
setattr(cp, field, False)
def test_factory_defaults_and_hidden_vision_control_producer(tmp_path):
params, cp = Params(str(tmp_path)), toyota()
settings = slc_runtime_settings(params, cp, {})
assert settings.selection.slots[:2] == (Source.VISION, Source.DASHBOARD)
assert not settings.enabled and settings.display
assert enabled(params, cp, 'conditional', {})
assert enabled(params, cp, 'slc', {}) and enabled(params, cp, 'vision', {})
assert not enabled(params, cp, 'curve', {})
params.put_bool('ShowSpeedLimits', False, block=True)
params.put_bool('SpeedLimitController', True, block=True)
assert enabled(params, cp, 'vision', {}) and vision_control_enabled(params, cp)
def test_toyota_actual_default_plannerd_constructs_requested_hosts(tmp_path):
params, cp = Params(str(tmp_path)), toyota()
params.put('CarParams', cp.to_bytes(), block=True)
params.put_bool('CurveSpeedController', True, block=True)
class EndLoop(Exception):
pass
sm = Mock()
sm.update.side_effect = EndLoop
with (patch.object(plannerd, 'Params', return_value=params),
patch.object(plannerd, 'config_realtime_process'), patch.object(plannerd, 'set_core_affinity'),
patch.object(plannerd.messaging, 'sub_sock'),
patch.object(plannerd.messaging, 'SubMaster', return_value=sm) as subscriber,
patch.object(plannerd.messaging, 'PubMaster') as publisher,
patch.object(plannerd, 'ConditionalPlannerHost', wraps=plannerd.ConditionalPlannerHost) as conditional,
patch.object(plannerd, 'ProfileHost', wraps=ProfileHost) as profiles,
patch.object(plannerd, 'CurveHost', wraps=CurveHost),
patch.dict('os.environ', {}, clear=True)):
with pytest.raises(EndLoop):
plannerd.main()
conditional.assert_called_once()
profiles.assert_called_once()
assert 'slcState' in publisher.call_args.args[0]
assert 'slcCruiseCommand' in publisher.call_args.args[0]
for source in ('slcDashboardObservation', 'slcVisionObservation', 'mapdOut'):
assert source in subscriber.call_args.args[0]
assert plannerd.SavedSlcLifecycle(params, cp, {}).refresh(NOW)[0] is not None
def test_toyota_curve_host_cap_reaches_actual_native_mpc_and_inactive_axis_denies_it():
cp, host = toyota(), CurveHost(enabled=True, replay=True)
planner = LongitudinalPlanner(cp, init_v=25.0)
applied = []
for tick in range(1, 31):
stamp = NOW + tick * 50_000_000
sm = bus(stamp)
result = plannerd.update_curve_frame(planner, sm, cp, stamp, host=host)
assert result.ceiling_mps is not None
applied.append(planner.last_curve_ceiling_applied)
assert any(applied)
sm.messages['carControl'] = sm.messages['carControl'].as_builder()
sm.messages['carControl'].longActive = False
result = plannerd.update_curve_frame(planner, sm, cp, stamp, host=host)
assert result.ceiling_mps is None and not planner.last_curve_ceiling_applied
def test_toyota_profile_host_and_slc_ceiling_reach_actual_native_planner(tmp_path):
params, cp = Params(str(tmp_path)), toyota()
params.put_bool('CustomPersonalities', True, block=True)
params.put('LongitudinalPersonalityProfiles', profile_document(default_personality_profiles(True), enabled=True), block=True)
host = ProfileHost(params)
planner = LongitudinalPlanner(cp, init_v=25.0)
ceiling = CruiseCeiling(10.0, Authority(Mode.LONGITUDINAL_ONLY, LongitudinalOwner.SYSTEM, False, True, False, False))
sm = bus(NOW)
target = plannerd.profile_for_frame(host, sm, cp, NOW)
assert target is not None
planner.update(sm, profile_tuning=target, cruise_ceiling=ceiling, now_ns=NOW)
assert planner.last_profile is not None
assert planner.last_cruise_ceiling_status == 'applied'
sm.messages['carControl'] = sm.messages['carControl'].as_builder()
sm.messages['carControl'].longActive = False
planner.update(sm, profile_tuning=target, now_ns=NOW)
assert planner.last_profile is None
owner = FeatureSettingsOwner(params, lambda group: True, vehicle_fingerprint=lambda: cp.carFingerprint, vehicle_params=lambda: cp)
assert owner.long_profiles.capability() is not None
@pytest.fixture
def mode_transport():
from openpilot.starpilot.conditional_mode.tests import test_status_consumer as fixture
with patch.object(fixture, 'CP', toyota()):
case = fixture.TestModeTransport('test_actual_selfdrived_owner_preserves_stock_and_separates_axes')
case.setUp()
try:
yield case
finally:
case.doCleanups()
def test_toyota_actual_selfdrived_acceptance_keeps_current_axis_and_freshness(mode_transport):
from openpilot.cereal import messaging
from openpilot.selfdrive.selfdrived.selfdrived import SelfdriveD
from openpilot.starpilot.conditional_mode.policy import ManualIntent
from openpilot.starpilot.conditional_mode.tests.test_projection import MONO, BOOT
case = mode_transport
drive = SelfdriveD.__new__(SelfdriveD)
drive.conditional_replay = False
drive.requested_experimental_mode = True
drive.update_conditional_mode(None)
case.assertTrue(drive.experimental_mode)
case.params.put_bool('ExperimentalMode', True, block=True)
case.owner.refresh(MONO, force=True)
drive.conditional_replay = True
drive.conditional_settings = case.owner
drive.conditional_consumer = case.consumer
drive.conditional_car_state_valid = True
drive.initialized = True
drive.enabled = True
drive.aol_replay = True
axes = NS(longitudinal_active=True, lateral_active=False)
case.enterContext(patch.object(drive, "aol_axis_decision", axes, create=True))
drive.CP = toyota()
for index, intent in enumerate((ManualIntent.FORCE_CHILL, ManualIntent.FORCE_EXPERIMENTAL), start=1):
now = MONO + index * 50_000_000
event, _, sm = case.produce(now, intent=intent)
device = messaging.new_message('deviceState', valid=True)
device.deviceState.started = True
device.deviceState.startedMonoTime = case.drive_id
sm.payloads['deviceState'] = messaging.log_from_bytes(device.to_bytes()).deviceState
sm.payloads['slcState'] = messaging.log_from_bytes(event.as_reader().as_builder().to_bytes()).slcState
for service in ('deviceState', 'slcState'):
sm.logMonoTime[service] = now
sm.recv_time[service] = now / 1e9
sm.seen[service] = sm.alive[service] = sm.valid[service] = True
sm.valid['slcState'] = False # Unavailable SLC does not invalidate this nested proposal.
drive.sm = sm
drive.aol_car_state_log_ns = now - 2_000_000
with patch('openpilot.selfdrive.selfdrived.selfdrived.paired_clocks_ns', return_value=(now, BOOT + now - MONO, 1000)):
drive.update_conditional_mode(sm['carState'])
case.assertEqual(drive.experimental_mode, intent is ManualIntent.FORCE_EXPERIMENTAL)
axes.longitudinal_active = False
drive.update_conditional_mode(sm['carState'])
case.assertTrue(drive.experimental_mode) # Exact existing stock request; no conditional override.
axes.longitudinal_active = True
def test_toyota_actual_conditional_planner_host_uses_native_mpc_and_adjacent_radar():
# Reuse the serialized fresh-source integration trace with Toyota's unchanged
# native CarParams, including pcmCruise=True, rather than a modified CP stub.
from openpilot.selfdrive.controls.tests import test_conditional_runtime_loop as fixture
case = fixture.TestConditionalRuntimeLoop('test_automatic_chill_joins_native_mpc_radar_and_disabled_slc')
with patch.object(fixture, 'CarInterface', NS(get_non_essential_params=lambda candidate: toyota())):
case.test_automatic_chill_joins_native_mpc_radar_and_disabled_slc()
def test_toyota_galaxy_long_authority_and_native_curve_rows(tmp_path):
from openpilot.starpilot.galaxy.settings import AuthorityContext, _qualified
cp = toyota()
assert _qualified(AuthorityContext(parked=True, cp=cp, cp_raw=cp.to_bytes()), 'long')
params = Params(str(tmp_path))
owner = FeatureSettingsOwner(params, lambda group: True, vehicle_fingerprint=lambda: cp.carFingerprint, vehicle_params=lambda: cp)
snapshot = owner.snapshot('curve', parked=True, system_long=True, lateral_context=True, metric=True)
master = next(row for row in snapshot.rows if row.key == 'CurveSpeedController')
assert master.available and 'not supported' not in master.reason
def test_invalid_saved_sources_and_explicit_replay_optins(tmp_path):
from pathlib import Path
params, cp = Params(str(tmp_path)), toyota()
for key, value, feature in (('ConditionalModeConfig', b'{bad', 'conditional'),
('CurveSpeedController', b'true', 'curve'),
('LongitudinalPersonalityProfiles', b'{bad', 'profile')):
Path(params.get_param_path(key)).write_bytes(value)
assert not enabled(params, cp, feature, {})
for feature, flag in (('conditional', 'CONDITIONAL_MODE_REPLAY_RUNTIME'),
('curve', 'CURVE_REPLAY_RUNTIME'), ('profile', 'LONG_PLANNER_REPLAY_RUNTIME'),
('slc', 'SLC_REPLAY_RUNTIME')):
assert enabled(params, None, feature, {flag: '1'})
assert enabled(params, None, 'vision', {'SLC_REPLAY_RUNTIME': '1', 'SLC_VISION_DEVELOPMENT': '1'})
assert not enabled(params, None, 'vision', {'SLC_VISION_DEVELOPMENT': '1'})
@@ -37,8 +37,7 @@ from openpilot.starpilot.ui.feature_settings_state import (
)
from openpilot.starpilot.speed_limits.runtime_settings import MPH_TO_MPS
from openpilot.starpilot.aol.vehicle import policy_for as aol_policy_for
from openpilot.starpilot.longitudinal.ioniq6_start import eligible as ioniq6_long_eligible
from opendbc.car.gm.feature_capabilities import longitudinal_supported as gm_longitudinal_supported
from openpilot.starpilot.feature_capabilities import longitudinal_supported as feature_longitudinal_supported
from openpilot.starpilot.lateral.torque_settings import DOCUMENT_KEY, LEGACY_KEYS
from openpilot.starpilot.ui.torque_feature import TorqueFeature
from openpilot.starpilot.ui.controller_feature import ControllerFeature, SETUP_ACTION
@@ -812,7 +811,7 @@ class FeatureSettingsOwner:
title = "Curve Speed Controller"
allowed = configurable and system_long and self.authority("long")
vehicle_cp = self.vehicle_params()
curve_supported = vehicle_cp is not None and (ioniq6_long_eligible(vehicle_cp) or gm_longitudinal_supported(vehicle_cp))
curve_supported = feature_longitudinal_supported(vehicle_cp)
learning = learning_snapshot(self.params)
document_raw = self._raw(CURVE_DOCUMENT_KEY)
dependencies = ((CURVE_DOCUMENT_KEY, document_raw),)
@@ -64,7 +64,7 @@ class LongProfileFeature:
def capability(self) -> tuple | None:
cp = self.owner.vehicle_params()
try:
if (cp is None or not cp.carFingerprint or not self.owner.longitudinal_available() or (cp.pcmCruise and not self.owner.configuration_longitudinal()) or
if (cp is None or not cp.carFingerprint or not self.owner.longitudinal_available() or
cp.passive or cp.notCar or cp.dashcamOnly):
return None
return (str(cp.carFingerprint), self.owner.longitudinal_available(), bool(cp.pcmCruise and not self.owner.configuration_longitudinal()),
+1 -1
View File
@@ -668,7 +668,7 @@ class StarShellSession:
return bool(cp.openpilotLongitudinalControl and not cp.notCar and not cp.passive and not cp.dashcamOnly)
if group in ("aol_wheel", "lane", "torque", "aol"):
return not cp.notCar and not cp.passive and not cp.dashcamOnly
return bool(cp.openpilotLongitudinalControl and not cp.pcmCruise and not cp.notCar and
return bool(cp.openpilotLongitudinalControl and not cp.notCar and
not cp.passive and not cp.dashcamOnly)
return False
+1 -1
View File
@@ -562,7 +562,7 @@ class RuntimeSnapshotAdapter:
cruise = None
stock_cruise = bool(display_car_valid and
bool(getattr(getattr(display_car, "cruiseState", None), "enabled", False)) and
cp is not None and (not cp.openpilotLongitudinalControl or cp.pcmCruise))
cp is not None and not cp.openpilotLongitudinalControl)
observation = speed_limit_from_message(slc) if slc is not None and (slc.enabled or slc.displayOnly) else SpeedLimitObservation()
if slc is None and started:
observation = SpeedLimitObservation(kind=ObservationKind.STALE)
@@ -314,11 +314,20 @@ class ConditionalFeatureTests(unittest.TestCase):
with patch.object(shell, "confirmed_offroad", return_value=True), \
patch.object(runtime_app.ui_state, "CP", self.cp):
self.assertTrue(shell._feature_authority("conditional"))
self.assertFalse(shell._feature_authority("long"))
self.assertTrue(shell._feature_authority("long"))
assert self.cp is not None
self.cp.openpilotLongitudinalControl = False
self.assertFalse(shell._feature_authority("conditional"))
self.assertFalse(shell._feature_authority("long"))
self.assertTrue(shell._feature_authority("preferences"))
self.cp.openpilotLongitudinalControl = True
for field in ('passive', 'dashcamOnly', 'notCar'):
with self.subTest(field=field):
setattr(self.cp, field, True)
self.assertFalse(shell._feature_authority("conditional"))
self.assertFalse(shell._feature_authority("long"))
self.assertTrue(shell._feature_authority("preferences"))
setattr(self.cp, field, False)
def test_native_saved_actions_use_settings_mode_and_supported_current_car(self):
from openpilot.starpilot.ui import runtime_app
@@ -123,7 +123,7 @@ class FeatureNavigationTests(unittest.TestCase):
session.feature_owner = FeatureSettingsOwner(params, session._feature_authority,
vehicle_fingerprint=lambda: vehicle.carFingerprint, vehicle_params=lambda: vehicle)
with patch.object(runtime_app, 'ui_state', NS(CP=vehicle)):
self.assertFalse(session._feature_authority('long'))
self.assertTrue(session._feature_authority('long'))
self.assertTrue(session._feature_authority('toyota_cruise'))
for expected in (True, False):
state = session.feature_owner.snapshot('profiles', parked=True, system_long=False,
@@ -144,7 +144,7 @@ class FeatureNavigationTests(unittest.TestCase):
other = HondaCarInterface.get_non_essential_params(HONDA_CAR.HONDA_CIVIC)
with patch.object(runtime_app, 'ui_state', NS(CP=other)):
self.assertFalse(session._feature_authority('toyota_cruise'))
self.assertFalse(session._feature_authority('long'))
self.assertTrue(session._feature_authority('long'))
def test_native_saved_preferences_need_settings_or_favorite_context_not_cp(self):
from openpilot.starpilot.ui import runtime_app
@@ -154,9 +154,14 @@ class FeatureNavigationTests(unittest.TestCase):
session = runtime_app.StarShellSession.__new__(runtime_app.StarShellSession)
session._mode = runtime_app.ShellMode.SETTINGS
with patch.object(session, "confirmed_offroad", return_value=True), patch.object(runtime_app, "ui_state", NS(CP=cp)):
self.assertTrue(session._feature_authority("long"))
self.assertTrue(session._feature_authority("preferences"))
cp.openpilotLongitudinalControl = False
self.assertFalse(session._feature_authority("long"))
self.assertTrue(session._feature_authority("preferences"))
cp.openpilotLongitudinalControl = True
cp.passive = True
self.assertFalse(session._feature_authority("long"))
self.assertTrue(session._feature_authority("preferences"))
with patch.object(session, "confirmed_offroad", return_value=True), patch.object(runtime_app, "ui_state", NS(CP=None)):
self.assertTrue(session._feature_authority("preferences"))
+8 -4
View File
@@ -2,7 +2,7 @@ import hashlib
from openpilot.starpilot.aol.vehicle import policy_for
from openpilot.starpilot.conditional_mode.manual import ioniq6_media_eligible
from openpilot.starpilot.controllers.wheel_actions import ACTIONS, DEFAULTS, KEYS, eligible
from openpilot.starpilot.controllers.wheel_actions import ACTIONS, DEFAULTS, KEYS, button_reported, eligible
from openpilot.starpilot.saved_document import commit_exact
from openpilot.starpilot.saved_source import read_saved
from openpilot.starpilot.ui.feature_settings_state import FeatureRow
@@ -38,9 +38,12 @@ class WheelFeature:
from openpilot.starpilot.car.hyundai.settings import configuration_wheel_policy, configuration_media_supported
policy = configuration_wheel_policy(cp) or policy
media = configuration_media_supported(cp) or media
if key == "LKASButtonControl" and not policy.lkas_button_supported:
if not button_reported(cp, key):
return ()
if key in KEYS[:2] and policy.fixed_cruise_buttons:
if key == "LKASButtonControl" and not (policy.lkas_button_supported or policy.auxiliary_lkas_supported):
return ()
if key in KEYS[:2] and policy.fixed_cruise_buttons and not (
key == "LKASButtonControl" and policy.auxiliary_lkas_supported):
return ()
if key in KEYS[5:8] and cp.brand == 'gm':
from opendbc.car.gm.cancel_remap import supported
@@ -48,7 +51,8 @@ class WheelFeature:
return ()
if key in KEYS[8:] and not media:
return ()
if key in KEYS[:5] and not policy.settings_supported:
if key in KEYS[:5] and not policy.settings_supported and not (
key == "LKASButtonControl" and policy.auxiliary_lkas_supported):
return ()
codes = {0, 8, 11, 12, 13}
if self.owner.longitudinal_available():