Files
onepilot/sunnypilot/sunnylink/capabilities.py
T
github-actions[bot] 6e2ccc8b15 sunnypilot v2026.002.000
version: sunnypilot v2026.002.000 (staging)
date: 2026-05-27T04:05:25
master commit: dfc3c98b226da57a653daf57131a8a3d66166fcb
2026-05-27 04:05:25 +00:00

187 lines
7.2 KiB
Python

"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import json
from cereal import car, custom, messaging
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, UNSUPPORTED_LONGITUDINAL_CAR
from opendbc.car.subaru.values import CAR as SUBARU_CAR, SubaruFlags
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware import HARDWARE
# Wire-protocol version for the capabilities payload. Bump on breaking changes
# only; additive fields are backward-compatible and do not require a bump.
PROTOCOL_VERSION = 1
# All capability fields that rules may reference.
# Non-boolean fields must have defaults in CAPABILITY_DEFAULTS.
CAPABILITY_FIELDS = (
"protocol_version",
"has_longitudinal_control",
"has_icbm",
"icbm_available",
"torque_allowed",
"brand",
"pcm_cruise",
"alpha_long_available",
"steer_control_type",
"enable_bsm",
"is_release",
"is_sp_release",
"is_development",
"tesla_has_vehicle_bus",
"has_stop_and_go",
"stock_longitudinal",
"device_type",
"subaru_has_sng",
"hyundai_alpha_long_available",
)
CAPABILITY_LABELS: dict[str, str] = {
"protocol_version": "Capabilities protocol version",
"has_longitudinal_control": "sunnypilot longitudinal control",
"has_icbm": "ICBM enabled",
"icbm_available": "ICBM available",
"torque_allowed": "torque steering (not available for angle steering vehicles)",
"brand": "Vehicle brand",
"pcm_cruise": "PCM cruise",
"alpha_long_available": "Alpha Longitudinal available",
"steer_control_type": "Steer control type",
"enable_bsm": "BSM available",
"is_release": "Release branch",
"is_sp_release": "SP release branch",
"is_development": "Development branch",
"tesla_has_vehicle_bus": "Tesla vehicle bus",
"has_stop_and_go": "Stop and Go",
"stock_longitudinal": "stock longitudinal",
"device_type": "Device type",
"subaru_has_sng": "Subaru Stop-and-Go available",
"hyundai_alpha_long_available": "Hyundai Alpha Longitudinal available",
}
# Explicit defaults for non-boolean capability fields
CAPABILITY_DEFAULTS: dict[str, bool | str | int] = {
"brand": "",
"steer_control_type": "",
"device_type": "",
"protocol_version": PROTOCOL_VERSION,
}
def _bundle_field(bundle: dict | None, key: str) -> str:
return bundle.get(key, "") if isinstance(bundle, dict) else ""
def _resolve_brand_capabilities(caps: dict, bundle_platform: str, CP) -> None:
"""Set brand-specific capabilities from bundle platform or CarParams fallback.
Bundle (manual car selection) is a pre-fingerprint approximation.
CarParams (auto-fingerprint) is the authoritative post-fingerprint source.
Mirrors the per-brand update_settings() logic in device UI layouts.
"""
brand = caps["brand"]
if brand == "hyundai":
if bundle_platform:
try:
unsupported = set().union(*UNSUPPORTED_LONGITUDINAL_CAR.values())
caps["hyundai_alpha_long_available"] = HYUNDAI_CAR[bundle_platform] not in unsupported
except KeyError:
cloudlog.exception(f"capabilities: unknown hyundai platform {bundle_platform!r}")
elif CP is not None:
caps["hyundai_alpha_long_available"] = bool(CP.alphaLongitudinalAvailable)
elif brand == "subaru":
if bundle_platform:
try:
flags = SUBARU_CAR[bundle_platform].config.flags
caps["subaru_has_sng"] = not bool(flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
caps["has_stop_and_go"] = caps["subaru_has_sng"]
except KeyError:
cloudlog.exception(f"capabilities: unknown subaru platform {bundle_platform!r}")
elif CP is not None:
caps["subaru_has_sng"] = not bool(CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
caps["has_stop_and_go"] = caps["subaru_has_sng"]
def generate_capabilities(params: Params | None = None) -> dict:
"""Generate a SettingsCapabilities dict from CarParams + boolean params.
When CarPlatformBundle is present, brand and platform come from the bundle
(mirrors Raylib). CarParams* deserialization is the fallback before the bundle
is written (early after first pairing).
"""
params = params or Params()
caps: dict = {field: CAPABILITY_DEFAULTS.get(field, False) for field in CAPABILITY_FIELDS}
# Wire-protocol version is always set explicitly.
caps["protocol_version"] = PROTOCOL_VERSION
# Hardware + boolean params (no CarParams dependency)
caps["device_type"] = HARDWARE.get_device_type()
caps["is_release"] = False # params.get_bool("IsReleaseBranch")
caps["is_sp_release"] = params.get_bool("IsReleaseSpBranch")
caps["is_development"] = params.get_bool("IsDevelopmentBranch")
caps["stock_longitudinal"] = params.get_bool("ToyotaEnforceStockLongitudinal")
bundle = params.get("CarPlatformBundle")
bundle_brand = _bundle_field(bundle, "brand")
bundle_platform = _bundle_field(bundle, "platform")
# Bundle-first brand resolution; CP is fallback only.
if bundle_brand:
caps["brand"] = bundle_brand
# CarParams-derived capabilities
CP = None
CP_bytes = params.get("CarParamsPersistent")
if CP_bytes is not None:
try:
CP = messaging.log_from_bytes(CP_bytes, car.CarParams)
caps["alpha_long_available"] = bool(CP.alphaLongitudinalAvailable)
if CP.alphaLongitudinalAvailable:
caps["has_longitudinal_control"] = params.get_bool("AlphaLongitudinalEnabled")
else:
caps["has_longitudinal_control"] = bool(CP.openpilotLongitudinalControl)
# CP.steerControlType is the physical control mode (angle / torque).
# CP.lateralTuning.which() returns the tuning class (pid / torque / indi)
# which is a separate concept and is not interchangeable.
caps["steer_control_type"] = str(CP.steerControlType)
caps["torque_allowed"] = CP.steerControlType != car.CarParams.SteerControlType.angle
if not caps["brand"] and CP.brand:
caps["brand"] = str(CP.brand)
caps["pcm_cruise"] = bool(CP.pcmCruise)
caps["enable_bsm"] = bool(CP.enableBsm)
# Generic SnG fallback. Brand-specific opaque flags below override.
caps["has_stop_and_go"] = bool(CP.openpilotLongitudinalControl)
except Exception:
CP = None
cloudlog.exception("capabilities: failed to deserialize CarParamsPersistent")
# CarParamsSP-derived capabilities
CP_SP_bytes = params.get("CarParamsSPPersistent")
if CP_SP_bytes is not None:
try:
CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
caps["icbm_available"] = bool(CP_SP.intelligentCruiseButtonManagementAvailable)
caps["has_icbm"] = bool(CP_SP.intelligentCruiseButtonManagementAvailable) and params.get_bool("IntelligentCruiseButtonManagement")
caps["tesla_has_vehicle_bus"] = bool(CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS)
except Exception:
cloudlog.exception("capabilities: failed to deserialize CarParamsSPPersistent")
_resolve_brand_capabilities(caps, bundle_platform, CP)
return caps
def generate_capabilities_json(params: Params | None = None) -> str:
"""Generate SettingsCapabilities as a JSON string."""
return json.dumps(generate_capabilities(params), separators=(",", ":"))