Files
StarPilot/openpilot/starpilot/lateral/controller_selection.py
T
firestar5683 2cd7728be2 FLM Auto-Weave
2026-10-17 18:38:27 -05:00

258 lines
11 KiB
Python

"""Startup-only selection between the standard torque controller and exact vehicle policies."""
from __future__ import annotations
from dataclasses import dataclass
from enum import StrEnum
import json
from pathlib import Path
from opendbc.car import structs
DOCUMENT_KEY = 'LateralControllerSelection'
DOCUMENT_VERSION = 1
LEARNING_OFF_KEY = 'ForceAutoTuneOff'
MAX_DOCUMENT_BYTES = 4096
KNOWN_POLICIES = {'HYUNDAI_IONIQ_6': 'hyundai', 'GENESIS_G70_2020': 'hyundai', 'GENESIS_GV70_ELECTRIFIED_1ST_GEN': 'hyundai', 'TOYOTA_COROLLA_TSS2': 'toyota'}
KNOWN_POLICIES.update({'HYUNDAI_SONATA_HYBRID': 'hyundai', 'TOYOTA_RAV4_TSS2': 'toyota'})
KNOWN_POLICIES.update(dict.fromkeys(('HYUNDAI_PALISADE', 'HYUNDAI_PALISADE_2023'), 'hyundai'))
KNOWN_POLICIES.update(dict.fromkeys((
'CHEVROLET_BOLT_CC_2017', 'CHEVROLET_BOLT_CC_2018_2021', 'CHEVROLET_BOLT_CC_2022_2023',
'CHEVROLET_BOLT_ACC_2022_2023', 'CHEVROLET_BOLT_ACC_2022_2023_PEDAL',
), 'gm'))
KNOWN_POLICIES.update(dict.fromkeys((
'CHEVROLET_VOLT', 'CHEVROLET_VOLT_ASCM', 'CHEVROLET_VOLT_CAMERA', 'CHEVROLET_VOLT_CC', 'CHEVROLET_VOLT_2019',
), 'gm'))
KNOWN_POLICIES.update(dict.fromkeys((
'CHEVROLET_MALIBU_ASCM', 'BUICK_LACROSSE_ASCM', 'BUICK_LACROSSE_ASCM_19US',
'CADILLAC_ESCALADE_ASCM', 'CADILLAC_ESCALADE_ESV_2019_ASCM', 'CHEVROLET_SUBURBAN_ASCM',
), 'gm'))
KNOWN_POLICIES.update(dict.fromkeys((
'CADILLAC_XT4', 'CHEVROLET_TRAVERSE', 'CADILLAC_XT5', 'CADILLAC_XT6',
'BUICK_BABYENCLAVE', 'CHEVROLET_BLAZER', 'CHEVROLET_MALIBU_SDGM',
), 'gm'))
KNOWN_POLICIES.update(dict.fromkeys((
'CADILLAC_CT6_CC', 'CADILLAC_XT4_CC', 'CADILLAC_XT5_CC', 'CHEVROLET_EQUINOX_CC',
'CHEVROLET_MALIBU_CC', 'CHEVROLET_MALIBU_HYBRID_CC', 'CHEVROLET_SUBURBAN_CC', 'CHEVROLET_TRAILBLAZER_CC', 'GMC_YUKON_CC',
), 'gm'))
KNOWN_POLICIES.update(dict.fromkeys((
'CHEVROLET_SILVERADO', 'CHEVROLET_EQUINOX', 'CHEVROLET_TRAILBLAZER', 'CHEVROLET_TRAX',
'GMC_YUKON', 'CHEVROLET_SUBURBAN_CAMERA', 'CHEVROLET_SILVERADO_CC', 'CHEVROLET_SUBURBAN',
), 'gm'))
KNOWN_POLICIES.update(dict.fromkeys((
'HYUNDAI_IONIQ_5',
'HYUNDAI_IONIQ_EV_LTD', 'HYUNDAI_IONIQ_EV_2020',
'HYUNDAI_SONATA',
'HYUNDAI_ELANTRA_2022_NON_SCC', 'HYUNDAI_ELANTRA_HEV_2022_NON_SCC',
'KIA_XCEED_PHEV',
'KIA_NIRO_EV', 'KIA_NIRO_PHEV_2022',
'KIA_STINGER_2022',
'KIA_FORTE', 'KIA_FORTE_2019_NON_SCC', 'KIA_FORTE_2021_NON_SCC',
'KIA_EV6',
'KIA_CARNIVAL_2025', 'KIA_CARNIVAL_HEV_4TH_GEN',
'HYUNDAI_TUCSON_4TH_GEN',
'HYUNDAI_KONA_NON_SCC',
'HYUNDAI_KONA_EV_2022',
), 'hyundai'))
class ControllerMode(StrEnum):
STANDARD = 'standard'
STARPILOT = 'starpilot'
@dataclass(frozen=True)
class ControllerSelection:
mode: ControllerMode
policy: str | None
source: str
def policy_for(CP) -> str | None:
"""Return only an already-implemented, exact torque policy identity."""
if CP.notCar:
return None
from openpilot.starpilot.lateral.hkg_vehicle_policy import policy_for as hkg_vehicle_policy
profile = hkg_vehicle_policy(CP)
if profile is not None:
return profile
from openpilot.starpilot.lateral.corolla_tss2_policy import supported_cp as corolla_supported
from openpilot.starpilot.lateral.genesis_g70_policy import supported_cp as g70_supported
from openpilot.starpilot.lateral.genesis_gv70_policy import supported_cp as gv70_supported
from openpilot.starpilot.lateral.bolt_policy import supported_cp as bolt_supported
from openpilot.starpilot.lateral.volt_policy import supported_cp as volt_supported
from openpilot.starpilot.lateral.ascm_policy import supported_cp as ascm_supported
from openpilot.starpilot.lateral.sdgm_policy import supported_cp as sdgm_supported
from openpilot.starpilot.lateral.ordinary_cc_policy import supported_cp as cc_supported
from openpilot.starpilot.lateral.camera_policy import supported_cp as camera_supported
from openpilot.starpilot.lateral.suburban_policy import supported_cp as suburban_supported
from opendbc.car.gm.values import is_silverado_cc_pedal_profile
if suburban_supported(CP):
return 'suburban'
if camera_supported(CP):
return 'silverado_cc' if is_silverado_cc_pedal_profile(CP) else 'ordinary_camera'
if cc_supported(CP):
return 'ordinary_cc'
if sdgm_supported(CP):
return 'ordinary_sdgm'
if ascm_supported(CP):
return 'ordinary_ascm'
if volt_supported(CP):
return 'volt'
if bolt_supported(CP):
return 'bolt'
if corolla_supported(CP):
return 'corolla_tss2'
from openpilot.starpilot.lateral.rav4_tss2_policy import supported_cp as rav4_supported
if rav4_supported(CP):
return 'rav4_tss2'
from openpilot.starpilot.lateral.palisade_policy import supported_cp as palisade_supported
from openpilot.starpilot.lateral.sonata_hybrid_policy import supported_cp as sonata_hybrid_supported
if sonata_hybrid_supported(CP):
return 'sonata_hybrid'
if palisade_supported(CP):
return 'palisade'
if gv70_supported(CP):
return 'genesis_gv70_electrified'
if g70_supported(CP):
return 'genesis_g70_2020'
if (str(CP.carFingerprint) == 'HYUNDAI_IONIQ_6' and str(CP.brand) == 'hyundai' and
CP.steerControlType == structs.CarParams.SteerControlType.torque and
CP.lateralTuning.which() == 'torque' and not CP.dashcamOnly and not CP.passive):
return 'ioniq6'
return None
def turn_assist_supported(CP) -> bool:
return bool(CP.steerControlType == structs.CarParams.SteerControlType.torque and
CP.lateralTuning.which() == 'torque' and not CP.notCar and not CP.passive and not CP.dashcamOnly and
any(config.safetyModel not in (structs.CarParams.SafetyModel.noOutput,
structs.CarParams.SafetyModel.silent, structs.CarParams.SafetyModel.allOutput)
for config in CP.safetyConfigs))
def model_turn_assist_supported(CP) -> bool:
return turn_assist_supported(CP) and policy_for(CP) == 'ioniq6'
def default_selection(CP) -> ControllerSelection:
policy = policy_for(CP)
from openpilot.starpilot.lateral.hkg_vehicle_policy import PROFILES
forte_non_scc = policy == 'kia_forte' and str(CP.carFingerprint) in ('KIA_FORTE_2019_NON_SCC', 'KIA_FORTE_2021_NON_SCC')
mode = ControllerMode.STARPILOT if policy and (policy not in PROFILES or forte_non_scc) else ControllerMode.STANDARD
return ControllerSelection(mode, policy, 'default')
def _unique_pairs(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError('Duplicate controller selection field')
result[key] = value
return result
def parse_document(raw: bytes) -> dict:
"""Reject unknown, ambiguous or partial documents before any saved edit."""
if len(raw) > MAX_DOCUMENT_BYTES:
raise ValueError('Oversize controller selection')
document = json.loads(raw.decode('utf-8'), object_pairs_hook=_unique_pairs)
if (not isinstance(document, dict) or set(document) != {'version', 'vehicles'} or
type(document['version']) is not int or document['version'] != DOCUMENT_VERSION):
raise ValueError('Invalid controller selection version')
vehicles = document['vehicles']
if not isinstance(vehicles, dict) or len(vehicles) > len(KNOWN_POLICIES):
raise ValueError('Invalid controller selection vehicles')
for fingerprint, choice in vehicles.items():
if (fingerprint not in KNOWN_POLICIES or not isinstance(choice, dict) or set(choice) != {'brand', 'mode'} or
choice['brand'] != KNOWN_POLICIES[fingerprint] or type(choice['mode']) is not str or
choice['mode'] not in (ControllerMode.STANDARD, ControllerMode.STARPILOT)):
raise ValueError('Invalid controller selection choice')
return document
def replace_mode(raw: bytes | None, CP, mode: ControllerMode) -> bytes:
"""Change only one exact platform's saved choice, preserving other entries."""
if policy_for(CP) is None:
raise ValueError('No exact torque policy for CarParams')
mode = ControllerMode(mode)
document = {'version': DOCUMENT_VERSION, 'vehicles': {}} if raw is None else parse_document(raw)
vehicles = dict(document['vehicles'])
vehicles[str(CP.carFingerprint)] = {'brand': str(CP.brand), 'mode': mode.value}
encoded = json.dumps({'version': DOCUMENT_VERSION, 'vehicles': vehicles},
sort_keys=True, separators=(',', ':'), allow_nan=False).encode()
if len(encoded) > MAX_DOCUMENT_BYTES:
raise ValueError('Oversize controller selection')
return encoded
def selection_from_bytes(CP, raw: bytes | None) -> ControllerSelection:
default = default_selection(CP)
if raw is None or default.policy is None:
return default
try:
choice = parse_document(raw)['vehicles'].get(str(CP.carFingerprint))
if choice is None or choice['brand'] != str(CP.brand):
return default
return ControllerSelection(ControllerMode(choice['mode']), default.policy, 'saved')
except (UnicodeError, ValueError, TypeError, OverflowError):
return ControllerSelection(default.mode, default.policy, 'invalid')
def read_selection(params, CP) -> ControllerSelection:
"""Read once at controls startup; a later settings write cannot change this controller."""
if policy_for(CP) is None:
return default_selection(CP)
try:
with Path(params.get_param_path(DOCUMENT_KEY)).open('rb') as source:
return selection_from_bytes(CP, source.read(MAX_DOCUMENT_BYTES + 1))
except FileNotFoundError:
return default_selection(CP)
except OSError:
default = default_selection(CP)
return ControllerSelection(default.mode, default.policy, 'invalid')
def controller_uses_native_learning(selection: ControllerSelection) -> bool:
return selection.mode in (ControllerMode.STANDARD, ControllerMode.STARPILOT) and selection.source != 'invalid'
def learning_allowed(params, CP, *, selection: ControllerSelection | None = None) -> bool:
"""Latch the exact controller's learning policy at each process startup.
An unrelated vehicle must not inherit another platform's learning preference.
Callers that already chose their controller supply that same startup selection.
"""
from openpilot.starpilot.lateral.learning_preferences import read_preference
preference = read_preference(params, CP)
if not preference.valid or not preference.enabled:
return False
if policy_for(CP) is None:
return True
selected = read_selection(params, CP) if selection is None else selection
from openpilot.starpilot.lateral.gm_geometry_runtime import read_geometry
geometry = read_geometry(params, CP)
if geometry.force_off:
return False
if geometry.force_auto:
return selected.source != 'invalid'
from openpilot.starpilot.lateral.torque_runtime import manual_overrides_present
if manual_overrides_present(CP, params):
return False
# Reuse the native estimator only in its existing brand scope. Actual
# controller parameter ownership still belongs to Controls or TorqueHost.
native_brand = str(CP.brand) in ('toyota', 'hyundai', 'rivian', 'honda', 'volkswagen')
return native_brand and controller_uses_native_learning(selected) and CP.lateralTuning.which() == 'torque'