This commit is contained in:
firestar5683
2026-09-15 20:23:01 -05:00
parent 1f8b955a0f
commit 26de11932a
14 changed files with 260 additions and 30 deletions
@@ -413,6 +413,12 @@ class CarState(CarStateBase):
else:
ret.gasPressed = bool(cp.vl["EMS16"]["CF_Ems_AclAct"])
if self.CP.carFingerprint == CAR.KIA_RAY_EV and self.CP.enableGasInterceptorDEPRECATED and self.ray_pedal_valid:
driver_pedal = cp_pedal.vl_raw["GAS_SENSOR"]
track1 = int.from_bytes(driver_pedal[:2], "big")
track2 = int.from_bytes(driver_pedal[2:4], "big")
ret.gasPressed = track1 > 272 or track2 > 513
# Gear Selection via Cluster - For those Kia/Hyundai which are not fully discovered, we can use the Cluster Indicator for Gear Selection,
# as this seems to be standard over all cars, but is not the preferred method.
if self.CP.flags & (HyundaiFlags.HYBRID | HyundaiFlags.EV):
@@ -91,6 +91,43 @@ def test_ray_pedal_fault_clears_only_with_healthy_sensor_state():
assert not ret.accFaulted
def test_ray_driver_override_uses_physical_interceptor_tracks():
CP = CarInterface.get_params(CAR.KIA_RAY_EV, ray_fingerprint(), [], False, False, False, None)
state = CarState(CP, None)
parsers = state.get_can_parsers(CP)
native_gas = (0x371, bytes.fromhex("004e008000ae0700"), 0)
physical_rest = (0x201, bytes.fromhex("010801f30cef"), 0)
for parser in parsers.values():
parser.update([(1_000_000_000, [native_gas, physical_rest])])
ret, _ = state.update(parsers, SimpleNamespace())
assert state.ray_pedal_valid
assert not ret.gasPressed
packer = CANPacker("hyundai_kia_ray_pedal")
physical_press = packer.make_can_msg("GAS_SENSOR", 0, {
"INTERCEPTOR_GAS": (310 - 264) * 0.672,
"INTERCEPTOR_GAS2": (593 - 497) * 0.332,
"STATE": 0, "COUNTER_PEDAL": 13,
})
for parser in parsers.values():
parser.update([(1_020_000_000, [physical_press])])
ret, _ = state.update(parsers, SimpleNamespace())
assert ret.gasPressed
def test_ray_without_pedal_keeps_native_gas_detection():
CP = CarInterface.get_params(CAR.KIA_RAY_EV, ray_fingerprint(sensor_length=8), [], False, False, False, None)
assert not CP.enableGasInterceptorDEPRECATED
state = CarState(CP, None)
parsers = state.get_can_parsers(CP)
assert Bus.party not in parsers
native_gas = (0x371, bytes.fromhex("004e008000ae0700"), 0)
for parser in parsers.values():
parser.update([(1_000_000_000, [native_gas])])
ret, _ = state.update(parsers, SimpleNamespace())
assert ret.gasPressed
def test_ray_controller_heartbeats_and_only_actuates_when_ready():
CP = CarInterface.get_params(CAR.KIA_RAY_EV, ray_fingerprint(), [], False, False, False, None)
controller = CarController(DBC[CP.carFingerprint], CP)
+5 -1
View File
@@ -259,7 +259,11 @@ static void hyundai_rx_hook(const CANPacket_t *msg) {
}
// gas press, different for EV, hybrid, and ICE models
if ((msg->addr == 0x371U) && hyundai_ev_gas_signal) {
if ((msg->addr == 0x201U) && hyundai_ray_pedal) {
const uint16_t track1 = ((uint16_t)msg->data[0] << 8U) | msg->data[1];
const uint16_t track2 = ((uint16_t)msg->data[2] << 8U) | msg->data[3];
gas_pressed = (track1 > 272U) || (track2 > 513U);
} else if ((msg->addr == 0x371U) && hyundai_ev_gas_signal && !hyundai_ray_pedal) {
gas_pressed = (((msg->data[4] & 0x7FU) << 1) | (msg->data[3] >> 7)) != 0U;
} else if ((msg->addr == 0x371U) && hyundai_hybrid_gas_signal) {
gas_pressed = msg->data[7] != 0U;
@@ -47,3 +47,38 @@ def test_ray_pedal_rx_crc_is_checked_only_for_ray_signature():
bad_crc = bytearray(dat)
bad_crc[-1] ^= 1
assert not safety.safety_rx_hook(libsafety_py.make_CANPacket(0x201, 0, bytes(bad_crc)))
def test_ray_native_commanded_gas_does_not_cancel_driver_override_safety():
safety = libsafety_py.libsafety
safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x9405)
safety.init_tests()
safety.set_controls_allowed(True)
packer = CANPacker("hyundai_kia_ray_pedal")
physical_rest = bytes.fromhex("010801f30cef")
native_gas = bytes.fromhex("004e008000ae0700")
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(0x201, 0, physical_rest))
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(0x371, 0, native_gas))
assert not safety.get_gas_pressed_prev()
addr, dat, bus = create_gas_interceptor_command(packer, 0.1, 3)
assert safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
physical_press = packer.make_can_msg("GAS_SENSOR", 0, {
"INTERCEPTOR_GAS": (310 - 264) * 0.672,
"INTERCEPTOR_GAS2": (593 - 497) * 0.332,
"STATE": 0, "COUNTER_PEDAL": 13,
})
press_addr, press_dat, press_bus = physical_press
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(press_addr, press_bus, press_dat))
assert safety.get_gas_pressed_prev()
assert not safety.safety_tx_hook(libsafety_py.make_CANPacket(addr, bus, dat))
def test_non_ray_hyundai_ev_keeps_native_driver_gas_detection():
safety = libsafety_py.libsafety
safety.set_safety_hooks(CarParams.SafetyModel.hyundai, 0x1001)
safety.init_tests()
native_gas = bytes.fromhex("004e008000ae0700")
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(0x371, 0, native_gas))
assert safety.get_gas_pressed_prev()
+10 -2
View File
@@ -195,12 +195,20 @@ class HudRenderer(Widget):
controls_state = sm['controlsState']
car_state = sm['carState']
rivian_lateral_mode.update()
car_control = sm['carControl'] if sm.valid.get('carControl', False) else None
actuators = getattr(car_control, "actuators", None)
long_active = bool(getattr(car_control, "longActive", False))
starpilot_car_state = sm['starpilotCarState'] if sm.valid.get('starpilotCarState', False) else None
pedal_feedback_enabled = ui_state.ui_params.get_bool("PedalsOnUI") or ui_state.ui_params.get_bool("ShowBrakeStatus")
self._wheel_tint = get_wheel_tint(
getattr(car_state, "brakePressed", False),
getattr(car_state, "brakePressed", False) or getattr(car_state, "regenBraking", False),
rivian_lateral_mode.wheel_tint,
ui_state.ui_params.get_bool("ShowBrakeStatus"),
pedal_feedback_enabled,
getattr(starpilot_car_state, "brakeLights", False),
getattr(car_state, "aEgo", 0.0),
getattr(car_state, "gasPressed", False),
getattr(actuators, "accel", 0.0) if long_active else 0.0,
getattr(actuators, "gas", 0.0) if long_active else 0.0,
)
v_cruise_cluster = car_state.vCruiseCluster
+30 -5
View File
@@ -14,11 +14,27 @@ from openpilot.starpilot.common.experimental_state import (
BRAKE_WHEEL_COLOR = rl.Color(255, 0, 0, 255)
ACCEL_WHEEL_COLOR = rl.Color(22, 127, 64, 255)
BRAKE_ACCEL_THRESHOLD = 0.25
COMMAND_ACCEL_THRESHOLD = 0.05
def get_wheel_tint(brake_pressed: bool, mode_tint: rl.Color | None, brake_status_enabled: bool,
brake_lights: bool = False) -> rl.Color | None:
return BRAKE_WHEEL_COLOR if brake_status_enabled and (brake_pressed or brake_lights) else mode_tint
def get_wheel_tint(brake_pressed: bool, mode_tint: rl.Color | None, pedal_feedback_enabled: bool,
brake_lights: bool = False, acceleration: float = 0.0,
gas_pressed: bool = False, commanded_accel: float = 0.0,
commanded_gas: float = 0.0) -> rl.Color | None:
if not pedal_feedback_enabled:
return mode_tint
braking = brake_pressed or brake_lights or acceleration < -BRAKE_ACCEL_THRESHOLD or \
commanded_accel < -COMMAND_ACCEL_THRESHOLD
accelerating = gas_pressed or acceleration > BRAKE_ACCEL_THRESHOLD or \
commanded_accel > COMMAND_ACCEL_THRESHOLD or commanded_gas > COMMAND_ACCEL_THRESHOLD
if braking:
return BRAKE_WHEEL_COLOR
if accelerating:
return ACCEL_WHEEL_COLOR
return mode_tint
class ExpButton(Widget):
@@ -112,12 +128,21 @@ class ExpButton(Widget):
texture = self._txt_exp if exp_mode else self._txt_wheel
color = self._white_color
tint = None
car_state = ui_state.sm["carState"]
car_control = ui_state.sm["carControl"] if getattr(ui_state.sm, "valid", {}).get("carControl", False) else None
actuators = getattr(car_control, "actuators", None)
long_active = bool(getattr(car_control, "longActive", False))
starpilot_car_state = ui_state.sm["starpilotCarState"] if getattr(ui_state.sm, "valid", {}).get("starpilotCarState", False) else None
pedal_feedback_enabled = self._params.get_bool("PedalsOnUI") or self._params.get_bool("ShowBrakeStatus")
wheel_tint = get_wheel_tint(
getattr(ui_state.sm["carState"], "brakePressed", False),
getattr(car_state, "brakePressed", False) or getattr(car_state, "regenBraking", False),
self.wheel_tint,
self._params.get_bool("ShowBrakeStatus"),
pedal_feedback_enabled,
getattr(starpilot_car_state, "brakeLights", False),
getattr(car_state, "aEgo", 0.0),
getattr(car_state, "gasPressed", False),
getattr(actuators, "accel", 0.0) if long_active else 0.0,
getattr(actuators, "gas", 0.0) if long_active else 0.0,
)
if wheel_tint is not None:
tint = rl.Color(wheel_tint.r, wheel_tint.g, wheel_tint.b, self._white_color.a)
+5 -1
View File
@@ -15,7 +15,11 @@ def render_pedal_icons(start_x: float, start_y: float, font):
return
standstill = getattr(car_state, "standstill", False)
brake_lights = getattr(car_state, "brakeLights", False)
starpilot_car_state = ui_state.sm["starpilotCarState"] if ui_state.sm.valid.get("starpilotCarState", False) else None
brake_lights = (getattr(car_state, "brakeLights", False) or
getattr(car_state, "brakeLightsDEPRECATED", False) or
getattr(car_state, "regenBraking", False) or
getattr(starpilot_car_state, "brakeLights", False))
acceleration_ego = getattr(car_state, "aEgo", 0.0)
dynamic_pedals = params.get_bool("DynamicPedalsOnUI")
+16 -2
View File
@@ -300,7 +300,7 @@ def test_non_mici_wheel_icon_turns_red_when_brakes_are_pressed(monkeypatch):
module, draws = load_exp_button(monkeypatch)
button = module.ExpButton(192, 144)
button.wheel_tint = FakeColor(0x4D, 0x9D, 0xFF, 255)
module.ui_state.ui_params.get_bool = lambda key, *args, **kwargs: key == "ShowBrakeStatus"
module.ui_state.ui_params.get_bool = lambda key, *args, **kwargs: key == "PedalsOnUI"
module.ui_state.sm["carState"].brakePressed = True
button._update_state()
@@ -311,6 +311,20 @@ def test_non_mici_wheel_icon_turns_red_when_brakes_are_pressed(monkeypatch):
assert (texture_color.r, texture_color.g, texture_color.b, texture_color.a) == (255, 0, 0, 255)
def test_non_mici_wheel_icon_turns_green_when_accelerating(monkeypatch):
module, draws = load_exp_button(monkeypatch)
button = module.ExpButton(192, 144)
button.wheel_tint = FakeColor(0x4D, 0x9D, 0xFF, 255)
module.ui_state.ui_params.get_bool = lambda key, *args, **kwargs: key == "PedalsOnUI"
module.ui_state.sm["carState"].aEgo = 0.4
button._update_state()
button._render(FakeRectangle(0, 0, 192, 192))
texture_color = draws["textures"][0][-1]
assert (texture_color.r, texture_color.g, texture_color.b, texture_color.a) == (22, 127, 64, 255)
def test_non_mici_wheel_icon_uses_reported_brake_lights(monkeypatch):
module, draws = load_exp_button(monkeypatch)
button = module.ExpButton(192, 144)
@@ -321,7 +335,7 @@ def test_non_mici_wheel_icon_uses_reported_brake_lights(monkeypatch):
module.ui_state.sm = FakeUiSubMaster(module.ui_state.sm)
module.ui_state.sm.valid = {"starpilotCarState": True}
module.ui_state.sm["starpilotCarState"] = SimpleNamespace(brakeLights=True)
module.ui_state.ui_params.get_bool = lambda key, *args, **kwargs: key == "ShowBrakeStatus"
module.ui_state.ui_params.get_bool = lambda key, *args, **kwargs: key == "PedalsOnUI"
button._update_state()
button._render(FakeRectangle(0, 0, 192, 192))
+4 -1
View File
@@ -13,6 +13,7 @@ from openpilot.selfdrive.ui.lib.prime_state import PrimeState
from openpilot.selfdrive.ui.lib.ui_param_cache import shared_ui_params
from openpilot.system.ui.lib.application import gui_app
from openpilot.starpilot.common.lateral_only_experimental import lateral_only_experimental_available
from openpilot.starpilot.common.car_params_capability import capability_car_params_bytes
from openpilot.system.hardware import HARDWARE, PC
from openpilot.starpilot.common.screen_settings import (
alert_wake_key, brightness_preferences, calculate_screen_brightness, enabled_wake_keys, standby_button_press_time,
@@ -284,7 +285,7 @@ class UIState:
def update_params(self) -> None:
# For slower operations
# Update longitudinal control state
CP_bytes = self.params.get("CarParamsPersistent")
CP_bytes = capability_car_params_bytes(self.params)
if CP_bytes is not None:
self.CP = messaging.log_from_bytes(CP_bytes, car.CarParams)
if self.CP.alphaLongitudinalAvailable:
@@ -296,6 +297,8 @@ class UIState:
lateral_only_experimental_available(self.CP)
)
else:
self.CP = None
self.has_longitudinal_control = False
self.experimental_mode_available = False
self._param_update_time = time.monotonic()
@@ -2768,9 +2768,9 @@
},
{
"key": "PedalsOnUI",
"label": "Gas / Brake Pedal Indicators",
"description": "On-screen gas and brake indicators.\n\nDynamic: Opacity changes according to how much openpilot is accelerating or braking\nStatic: Full when active, dim when not",
"picker_description": "Shows gas and brake indicators on the driving screen.",
"label": "Pedal-Responsive Wheel",
"description": "Tint the on-screen steering-wheel icon green while accelerating and red while braking. Also show gas and brake indicators.\n\nDynamic: Opacity changes according to how much openpilot is accelerating or braking\nStatic: Full when active, dim when not",
"picker_description": "Colors the driving-screen wheel by acceleration and braking, with optional pedal indicators.",
"data_type": "bool",
"ui_type": "toggle",
"is_parent_toggle": true,
@@ -2807,16 +2807,6 @@
"parent_key": "CustomUI",
"settings_tier": "simple"
},
{
"key": "ShowBrakeStatus",
"label": "Show Brake Status",
"description": "Tint the on-screen steering-wheel icon red while the car reports that its brake lights are on.",
"picker_description": "Tints the on-screen steering-wheel icon red while the brake lights are on.",
"data_type": "bool",
"ui_type": "toggle",
"galaxy_only": true,
"settings_tier": "simple"
},
{
"key": "ModelUI",
"label": "Model UI",
+39
View File
@@ -0,0 +1,39 @@
"""Choose a recognized CarParams snapshot for capability-gated settings.
The current persistent snapshot may temporarily be MOCK while the car process
starts. A matching previous route is safe for displaying an *available* toggle;
it never enables control or changes the panda safety configuration by itself.
"""
from cereal import car
def _fingerprint(raw: bytes | None) -> str:
if not raw:
return ""
try:
with car.CarParams.from_bytes(raw) as cp:
return str(cp.carFingerprint)
except Exception:
return ""
def capability_car_params_bytes(params) -> bytes | None:
"""Prefer live/recognized params; fallback only to the exact forced model."""
model = params.get("CarModel", encoding="utf-8") or ""
forced = params.get_bool("ForceFingerprint")
current_keys = ("CarParams", "CarParamsPersistent") if params.get_bool("IsOnroad") else ("CarParamsPersistent",)
for key in current_keys:
raw = params.get(key)
fingerprint = _fingerprint(raw)
if fingerprint and fingerprint != "MOCK":
if forced and model and fingerprint != model:
return None
return raw
if forced and model:
previous = params.get("CarParamsPrevRoute")
if _fingerprint(previous) == model:
return previous
return None
@@ -0,0 +1,63 @@
from cereal import car
from openpilot.starpilot.common.car_params_capability import capability_car_params_bytes
class FakeParams:
def __init__(self, values):
self.values = values
def get(self, key, encoding=None):
value = self.values.get(key)
return value.decode() if encoding and isinstance(value, bytes) else value
def get_bool(self, key):
return self.values.get(key) in (True, b"1", "1")
def cp_bytes(fingerprint, alpha_available=False):
cp = car.CarParams.new_message()
cp.carFingerprint = fingerprint
cp.alphaLongitudinalAvailable = alpha_available
return cp.to_bytes()
def test_matching_previous_route_unblocks_capability_after_transient_mock():
hw1 = cp_bytes("TESLA_MODEL_S_HW1", alpha_available=True)
params = FakeParams({
"CarModel": "TESLA_MODEL_S_HW1", "ForceFingerprint": True,
"CarParamsPersistent": cp_bytes("MOCK"), "CarParamsPrevRoute": hw1,
})
assert capability_car_params_bytes(params) == hw1
def test_live_recognized_params_take_precedence_over_previous_route():
hw1 = cp_bytes("TESLA_MODEL_S_HW1", alpha_available=True)
live = cp_bytes("TESLA_MODEL_S_HW1", alpha_available=False)
params = FakeParams({
"CarModel": "TESLA_MODEL_S_HW1", "ForceFingerprint": True, "IsOnroad": True,
"CarParams": live, "CarParamsPersistent": cp_bytes("MOCK"), "CarParamsPrevRoute": hw1,
})
assert capability_car_params_bytes(params) == live
def test_previous_route_must_match_explicit_forced_model():
previous = cp_bytes("TESLA_MODEL_S_HW1", alpha_available=True)
params = FakeParams({
"CarModel": "TESLA_MODEL_S_PREAP", "ForceFingerprint": True,
"CarParamsPersistent": cp_bytes("MOCK"), "CarParamsPrevRoute": previous,
})
assert capability_car_params_bytes(params) is None
params.values["CarModel"] = "TESLA_MODEL_S_HW1"
params.values["ForceFingerprint"] = False
assert capability_car_params_bytes(params) is None
def test_current_recognized_mismatch_blocks_stale_fallback():
params = FakeParams({
"CarModel": "TESLA_MODEL_S_HW1", "ForceFingerprint": True,
"CarParamsPersistent": cp_bytes("TESLA_MODEL_S_PREAP"),
"CarParamsPrevRoute": cp_bytes("TESLA_MODEL_S_HW1", alpha_available=True),
})
assert capability_car_params_bytes(params) is None
@@ -111,13 +111,14 @@ def test_driving_personality_controls_are_not_parked_only():
assert all(params[key].get("requires_offroad") is not True for key in personality_keys)
def test_brake_status_toggle_is_galaxy_only():
setting = _params_by_section(_layout())["Visual (Display & UI)"]["ShowBrakeStatus"]
def test_pedal_feedback_wheel_uses_existing_pedal_toggle():
setting = _params_by_section(_layout())["Visual (Display & UI)"]["PedalsOnUI"]
assert _declared_default("ShowBrakeStatus") == "0"
assert setting["galaxy_only"] is True
assert _declared_default("PedalsOnUI") == "0"
assert setting["label"] == "Pedal-Responsive Wheel"
assert setting["settings_tier"] == "simple"
assert setting["ui_type"] == "toggle"
assert "ShowBrakeStatus" not in _params_by_section(_layout())["Visual (Display & UI)"]
def test_ford_lateral_controls_are_ford_only_and_galaxy_only():
+2 -1
View File
@@ -78,6 +78,7 @@ from openpilot.starpilot.common.model_lab import (
)
from openpilot.starpilot.assets.theme_manager import HOLIDAY_THEME_PATH, THEME_COMPONENT_PARAMS
from openpilot.starpilot.common import param_profiles
from openpilot.starpilot.common.car_params_capability import capability_car_params_bytes
from openpilot.starpilot.system.the_galaxy import version_history, version_install
from openpilot.starpilot.common.accel_profile import (
A_CRUISE_MAX_BP_CUSTOM,
@@ -4463,7 +4464,7 @@ def _get_is_tici_or_tizi():
return HARDWARE.get_device_type() in ("tici", "tizi")
def _get_alpha_longitudinal_available():
cp_bytes = _safe_params_get_live_raw("CarParamsPersistent")
cp_bytes = capability_car_params_bytes(params)
if not cp_bytes:
return False