From 4d65c52e6d16ac1604d5d21d6f8e691391046bd2 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Thu, 5 Feb 2026 05:35:57 -0800 Subject: [PATCH 01/11] modeld_v2: refactor abstract class to support off-policy models (#1672) * modeld_v2: refactor abstract class to support off-policy models. * whoops * bump --- cereal/custom.capnp | 1 + release/ci/model_generator.py | 4 ++- sunnypilot/modeld_v2/SConscript | 4 +-- sunnypilot/modeld_v2/install_models_pc.py | 2 +- sunnypilot/models/fetcher.py | 2 +- .../models/runners/tinygrad/model_types.py | 16 ++++++++++ .../runners/tinygrad/tinygrad_runner.py | 32 +++++++++++++++---- 7 files changed, 50 insertions(+), 11 deletions(-) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 5c0a004fa6..53986262ec 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -153,6 +153,7 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { navigation @1; vision @2; policy @3; + offPolicy @4; } } diff --git a/release/ci/model_generator.py b/release/ci/model_generator.py index ee41343be8..96352254b6 100755 --- a/release/ci/model_generator.py +++ b/release/ci/model_generator.py @@ -68,8 +68,10 @@ def generate_metadata(model_path: Path, output_dir: Path, short_name: str): metadata_file = metadata_file.rename(output_path / f"{base}_{short_name.lower()}_metadata.pkl") # Build the metadata structure + model_type = "offPolicy" if "off_policy" in base else base.split("_")[-1] + model_metadata = { - "type": base.split("_")[-1] if "dmonitoring" not in base else "dmonitoring", + "type": model_type, "artifact": { "file_name": tinygrad_file.name, "download_uri": { diff --git a/sunnypilot/modeld_v2/SConscript b/sunnypilot/modeld_v2/SConscript index 48b9c75ef5..94033846b0 100644 --- a/sunnypilot/modeld_v2/SConscript +++ b/sunnypilot/modeld_v2/SConscript @@ -39,7 +39,7 @@ if PC: model_dir = Dir("models").abspath cmd = f'python3 {Dir("#sunnypilot/modeld_v2").abspath}/install_models_pc.py {model_dir}' - for model_name in ['supercombo', 'driving_vision', 'driving_policy']: + for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']: if File(f"models/{model_name}.onnx").exists(): inputs.append(File(f"models/{model_name}.onnx")) inputs.append(File(f"models/{model_name}_tinygrad.pkl")) @@ -57,7 +57,7 @@ def tg_compile(flags, model_name): ) # Compile small models -for model_name in ['supercombo', 'driving_vision', 'driving_policy']: +for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']: if File(f"models/{model_name}.onnx").exists(): flags = { 'larch64': 'DEV=QCOM', diff --git a/sunnypilot/modeld_v2/install_models_pc.py b/sunnypilot/modeld_v2/install_models_pc.py index 3f964dc285..a378d90b11 100755 --- a/sunnypilot/modeld_v2/install_models_pc.py +++ b/sunnypilot/modeld_v2/install_models_pc.py @@ -44,7 +44,7 @@ def generate_metadata_pkl(model_path, output_path): def install_models(model_dir): model_dir = Path(model_dir) - models = ["driving_policy", "driving_vision"] + models = ["driving_off_policy", "driving_policy", "driving_vision"] found_models = [] for model in models: diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index a917d6cbb8..1d8da083a9 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -116,7 +116,7 @@ class ModelCache: class ModelFetcher: """Handles fetching and caching of model data from remote source""" - MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v10.json" + MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v11.json" def __init__(self, params: Params): self.params = params diff --git a/sunnypilot/models/runners/tinygrad/model_types.py b/sunnypilot/models/runners/tinygrad/model_types.py index ba388aed93..11f0965828 100644 --- a/sunnypilot/models/runners/tinygrad/model_types.py +++ b/sunnypilot/models/runners/tinygrad/model_types.py @@ -13,6 +13,22 @@ SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') CUSTOM_MODEL_PATH = Paths.model_root() +class OffPolicyTinygrad(ModularRunner, ABC): + """ + A TinygradRunner specialized for off-policy models. + + Uses a SplitParser to handle outputs specific to the off-policy part of a split model setup. + """ + def __init__(self): + self._off_policy_parser = SplitParser() + self.parser_method_dict[ModelType.offPolicy] = self._parse_off_policy_outputs + + def _parse_off_policy_outputs(self, model_outputs: np.ndarray) -> NumpyDict: + """Parses off-policy model outputs using SplitParser.""" + result: NumpyDict = self._off_policy_parser.parse_policy_outputs(self._slice_outputs(model_outputs)) + return result + + class PolicyTinygrad(ModularRunner, ABC): """ A TinygradRunner specialized for policy-only models. diff --git a/sunnypilot/models/runners/tinygrad/tinygrad_runner.py b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py index 2800179fb2..7b48e3086b 100644 --- a/sunnypilot/models/runners/tinygrad/tinygrad_runner.py +++ b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py @@ -4,7 +4,7 @@ import numpy as np from openpilot.sunnypilot.modeld_v2.runners.tinygrad_helpers import qcom_tensor_from_opencl_address from openpilot.sunnypilot.models.runners.constants import CLMemDict, FrameDict, NumpyDict, ModelType, ShapeDict, CUSTOM_MODEL_PATH, SliceDict from openpilot.sunnypilot.models.runners.model_runner import ModelRunner -from openpilot.sunnypilot.models.runners.tinygrad.model_types import PolicyTinygrad, VisionTinygrad, SupercomboTinygrad +from openpilot.sunnypilot.models.runners.tinygrad.model_types import PolicyTinygrad, VisionTinygrad, SupercomboTinygrad, OffPolicyTinygrad from openpilot.system.hardware import TICI from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants from openpilot.sunnypilot.modeld_v2.constants import ModelConstants @@ -12,7 +12,7 @@ from openpilot.sunnypilot.modeld_v2.constants import ModelConstants from tinygrad.tensor import Tensor -class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTinygrad): +class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTinygrad, OffPolicyTinygrad): """ A ModelRunner implementation for executing Tinygrad models. @@ -27,6 +27,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny SupercomboTinygrad.__init__(self) PolicyTinygrad.__init__(self) VisionTinygrad.__init__(self) + OffPolicyTinygrad.__init__(self) self._constants = ModelConstants self._model_data = self.models.get(model_type) if not self._model_data or not self._model_data.model: @@ -106,13 +107,20 @@ class TinygradSplitRunner(ModelRunner): self.is_20hz_3d = True self.vision_runner = TinygradRunner(ModelType.vision) self.policy_runner = TinygradRunner(ModelType.policy) + self.off_policy_runner = TinygradRunner(ModelType.offPolicy) if self.models.get(ModelType.offPolicy) else None self._constants = SplitModelConstants def _run_model(self) -> NumpyDict: """Runs both vision and policy models and merges their parsed outputs.""" policy_output = self.policy_runner.run_model() vision_output = self.vision_runner.run_model() - return {**policy_output, **vision_output} # Combine results + outputs = {**policy_output, **vision_output} + + if self.off_policy_runner: + off_policy_output = self.off_policy_runner.run_model() + outputs.update(off_policy_output) + + return outputs @property def vision_input_names(self) -> list[str]: @@ -122,12 +130,18 @@ class TinygradSplitRunner(ModelRunner): @property def input_shapes(self) -> ShapeDict: """Returns the combined input shapes from both vision and policy models.""" - return {**self.policy_runner.input_shapes, **self.vision_runner.input_shapes} + shapes = {**self.policy_runner.input_shapes, **self.vision_runner.input_shapes} + if self.off_policy_runner: + shapes.update(self.off_policy_runner.input_shapes) + return shapes @property def output_slices(self) -> SliceDict: """Returns the combined output slices from both vision and policy models.""" - return {**self.policy_runner.output_slices, **self.vision_runner.output_slices} + slices = {**self.policy_runner.output_slices, **self.vision_runner.output_slices} + if self.off_policy_runner: + slices.update(self.off_policy_runner.output_slices) + return slices def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict: """Prepares inputs for both vision and policy models.""" @@ -135,5 +149,11 @@ class TinygradSplitRunner(ModelRunner): self.policy_runner.prepare_policy_inputs(numpy_inputs) # Vision inputs depend on imgs_cl and frames self.vision_runner.prepare_vision_inputs(imgs_cl, frames) + inputs = {**self.policy_runner.inputs, **self.vision_runner.inputs} + + if self.off_policy_runner: + self.off_policy_runner.prepare_policy_inputs(numpy_inputs) + inputs.update(self.off_policy_runner.inputs) + # Return combined inputs (though they are stored within respective runners) - return {**self.policy_runner.inputs, **self.vision_runner.inputs} + return inputs From c908189e73af3b3d15f1c1945d8b05dae035fd42 Mon Sep 17 00:00:00 2001 From: royjr Date: Sun, 8 Feb 2026 00:32:12 -0500 Subject: [PATCH 02/11] ui: use correct signals while using PID with Developer UI (#1674) * only if angleState * pidState element --- .../onroad/developer_ui/__init__.py | 7 ++++-- .../onroad/developer_ui/elements.py | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py b/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py index 441a169d23..f74f1f4c30 100644 --- a/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py +++ b/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py @@ -10,7 +10,7 @@ from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui.elements import ( UiElement, RelDistElement, RelSpeedElement, SteeringAngleElement, DesiredLateralAccelElement, ActualLateralAccelElement, DesiredSteeringAngleElement, AEgoElement, LeadSpeedElement, FrictionCoefficientElement, LatAccelFactorElement, - SteeringTorqueEpsElement, BearingDegElement, AltitudeElement + SteeringTorqueEpsElement, BearingDegElement, AltitudeElement, DesiredSteeringPIDElement ) from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.text_measure import measure_text_cached @@ -36,6 +36,7 @@ class DeveloperUiRenderer(Widget): self.desired_lat_accel_elem = DesiredLateralAccelElement() self.actual_lat_accel_elem = ActualLateralAccelElement() self.desired_steer_elem = DesiredSteeringAngleElement() + self.desired_pid_steer_elem = DesiredSteeringPIDElement() self.a_ego_elem = AEgoElement() self.lead_speed_elem = LeadSpeedElement() self.friction_elem = FrictionCoefficientElement() @@ -85,8 +86,10 @@ class DeveloperUiRenderer(Widget): ] if controls_state.lateralControlState.which() == 'torqueState': elements.append(self.desired_lat_accel_elem.update(sm, ui_state.is_metric)) - else: + elif controls_state.lateralControlState.which() == 'angleState': elements.append(self.desired_steer_elem.update(sm, ui_state.is_metric)) + elif controls_state.lateralControlState.which() == 'pidState': + elements.append(self.desired_pid_steer_elem.update(sm, ui_state.is_metric)) elements.append(self.actual_lat_accel_elem.update(sm, ui_state.is_metric)) diff --git a/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py b/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py index 652469bddd..94e3af42eb 100644 --- a/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py +++ b/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py @@ -191,6 +191,31 @@ class DesiredLateralAccelElement(LateralControlElement): return UiElement(value, "DESIRED L.A.", self.unit, color) +class DesiredSteeringPIDElement(LateralControlElement): + def __init__(self): + self.unit = "" + + def update(self, sm, is_metric: bool) -> UiElement: + car_state = sm['carState'] + controls_state = sm['controlsState'] + lat_active = sm['carControl'].latActive + angle_steers = car_state.steeringAngleDeg + steer_angle_desired = controls_state.lateralControlState.pidState.steeringAngleDesiredDeg + + value = f"{steer_angle_desired:.1f}°" if lat_active else "-" + + color = rl.WHITE + if lat_active: + if abs(angle_steers) > 180: + color = rl.RED + elif abs(angle_steers) > 90: + color = rl.Color(255, 188, 0, 255) + else: + color = rl.Color(0, 255, 0, 255) + + return UiElement(value, "DESIRED STEER", self.unit, color) + + class AEgoElement: def __init__(self): self.unit = "m/s^2" From 81bd8aa0e262ed2a5cfb024d1214d0a34231c352 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 8 Feb 2026 01:03:16 -0500 Subject: [PATCH 03/11] [bot] Update Python packages (#1662) * Update Python packages * bump --------- Co-authored-by: github-actions[bot] Co-authored-by: Jason Wen --- docs/CARS.md | 11 +---------- opendbc_repo | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index a1f50e41d0..ab1eabeb13 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,7 +4,7 @@ A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. -# 344 Supported Cars +# 335 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video|Setup Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| @@ -23,10 +23,7 @@ A supported vehicle is one that just works when you install a comma device. All |Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| |Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| -|Chevrolet|Bolt EV Non-ACC 2017|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| -|Chevrolet|Bolt EV Non-ACC 2018-21|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| |Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| -|Chevrolet|Malibu Non-ACC 2016-23|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| |Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| |Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| |Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -91,7 +88,6 @@ A supported vehicle is one that just works when you install a comma device. All |Honda|Civic Hatchback Hybrid 2025-26|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Civic Hybrid 2025-26|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Honda|Clarity 2018-21|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Nidec connector + Honda Clarity Proxy Board
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|CR-V 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|15 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|CR-V 2023-26|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -122,7 +118,6 @@ A supported vehicle is one that just works when you install a comma device. All |Hyundai|Elantra 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Elantra GT 2017-20|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Elantra Hybrid 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Hyundai|Elantra Non-SCC 2022|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai J connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|i30 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Ioniq 5 (Southeast Asia and Europe only) 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -140,9 +135,7 @@ A supported vehicle is one that just works when you install a comma device. All |Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai O connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Kona Electric (with HDA II, Korea only) 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai R connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Hyundai|Kona Electric Non-SCC 2019|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Kona Hybrid 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai I connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Hyundai|Kona Non-SCC 2019|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Nexo 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Palisade 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Hyundai|Santa Cruz 2022-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai N connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -166,13 +159,11 @@ A supported vehicle is one that just works when you install a comma device. All |Kia|Carnival 2022-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|Carnival (China only) 2023|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|Ceed 2019-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Kia|Ceed Plug-in Hybrid Non-SCC 2022|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai I connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|EV6 (Southeast Asia only) 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|EV6 (with HDA II) 2022-24|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|EV6 (without HDA II) 2022-24|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|Forte 2019-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|6 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|Forte 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Kia|Forte Non-SCC 2019|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|K5 2021-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|K5 Hybrid 2020-22|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|K8 Hybrid (with HDA II) 2023|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| diff --git a/opendbc_repo b/opendbc_repo index 1abdb9872f..ff2f9686c2 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 1abdb9872f22c361322ac9e29d376c35233ba890 +Subproject commit ff2f9686c208824a72b30d4cc540a6ab78c5983e From 35c87a151972c88f8251f8d877be615ced253334 Mon Sep 17 00:00:00 2001 From: royjr Date: Sun, 8 Feb 2026 17:00:37 -0500 Subject: [PATCH 04/11] [TIZI/TICI] ui: steering arc (#1628) * init * lint * add toggle * Update params_keys.h * Update params_metadata.json * Update params_keys.h * bool * decouple * no * make it perfect * fade it * only with torque bar * dynamic * in another PR --------- Co-authored-by: Jason Wen --- common/params_keys.h | 1 + selfdrive/ui/mici/onroad/torque_bar.py | 16 ++++++++------- selfdrive/ui/onroad/augmented_road_view.py | 8 +++++--- .../sunnypilot/onroad/augmented_road_view.py | 20 ++++++++++++++++++- .../ui/sunnypilot/onroad/hud_renderer.py | 9 +++++++++ selfdrive/ui/sunnypilot/ui_state.py | 1 + sunnypilot/sunnylink/params_metadata.json | 4 ++++ 7 files changed, 48 insertions(+), 11 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index ecc656cc78..44584c9113 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -115,6 +115,7 @@ inline static std::unordered_map keys = { {"SnoozeUpdate", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, {"SshEnabled", {PERSISTENT | BACKUP, BOOL}}, {"TermsVersion", {PERSISTENT, STRING}}, + {"TorqueBar", {PERSISTENT | BACKUP, BOOL, "0"}}, {"TrainingVersion", {PERSISTENT, STRING}}, {"UbloxAvailable", {PERSISTENT, BOOL}}, {"UpdateAvailable", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, diff --git a/selfdrive/ui/mici/onroad/torque_bar.py b/selfdrive/ui/mici/onroad/torque_bar.py index c8485a3101..c1de694633 100644 --- a/selfdrive/ui/mici/onroad/torque_bar.py +++ b/selfdrive/ui/mici/onroad/torque_bar.py @@ -146,9 +146,11 @@ def arc_bar_pts(cx: float, cy: float, class TorqueBar(Widget): - def __init__(self, demo: bool = False): + def __init__(self, demo: bool = False, scale: float = 1.0, always: bool = False): super().__init__() self._demo = demo + self._scale = scale + self._always = always self._torque_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) self._torque_line_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) @@ -180,8 +182,8 @@ class TorqueBar(Widget): def _render(self, rect: rl.Rectangle) -> None: # adjust y pos with torque - torque_line_offset = np.interp(abs(self._torque_filter.x), [0.5, 1], [22, 26]) - torque_line_height = np.interp(abs(self._torque_filter.x), [0.5, 1], [14, 56]) + torque_line_offset = np.interp(abs(self._torque_filter.x), [0.5, 1], [22 * self._scale, 26 * self._scale]) + torque_line_height = np.interp(abs(self._torque_filter.x), [0.5, 1], [14 * self._scale, 56 * self._scale]) # animate alpha and angle span if not self._demo: @@ -195,7 +197,7 @@ class TorqueBar(Widget): torque_line_bg_color = rl.Color(255, 255, 255, int(255 * 0.15 * self._torque_line_alpha_filter.x)) # draw curved line polygon torque bar - torque_line_radius = 1200 + torque_line_radius = 1200 * self._scale top_angle = -90 torque_bg_angle_span = self._torque_line_alpha_filter.x * TORQUE_ANGLE_SPAN torque_start_angle = top_angle - torque_bg_angle_span / 2 @@ -207,13 +209,13 @@ class TorqueBar(Widget): cy = rect.y + rect.height + torque_line_radius - torque_line_offset # draw bg torque indicator line - bg_pts = arc_bar_pts(cx, cy, mid_r, torque_line_height, torque_start_angle, torque_end_angle) + bg_pts = arc_bar_pts(cx, cy, mid_r, torque_line_height, torque_start_angle, torque_end_angle, cap_radius=7 * self._scale) draw_polygon(rect, bg_pts, color=torque_line_bg_color) # draw torque indicator line a0s = top_angle a1s = a0s + torque_bg_angle_span / 2 * self._torque_filter.x - sl_pts = arc_bar_pts(cx, cy, mid_r, torque_line_height, a0s, a1s) + sl_pts = arc_bar_pts(cx, cy, mid_r, torque_line_height, a0s, a1s, cap_radius=7 * self._scale) # draw beautiful gradient from center to 65% of the bg torque bar width start_grad_pt = cx / rect.width @@ -252,5 +254,5 @@ class TorqueBar(Widget): # draw center torque bar dot if abs(self._torque_filter.x) < 0.5: dot_y = self._rect.y + self._rect.height - torque_line_offset - torque_line_height / 2 - rl.draw_circle(int(cx), int(dot_y), 10 // 2, + rl.draw_circle(int(cx), int(dot_y), (10 // 2 * self._scale), rl.Color(182, 182, 182, int(255 * 0.9 * self._torque_line_alpha_filter.x))) diff --git a/selfdrive/ui/onroad/augmented_road_view.py b/selfdrive/ui/onroad/augmented_road_view.py index bcbcb2dcfb..76e7b078d1 100644 --- a/selfdrive/ui/onroad/augmented_road_view.py +++ b/selfdrive/ui/onroad/augmented_road_view.py @@ -15,7 +15,7 @@ from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCamera from openpilot.common.transformations.orientation import rot_from_euler if gui_app.sunnypilot_ui(): - from openpilot.selfdrive.ui.sunnypilot.onroad.augmented_road_view import BORDER_COLORS_SP + from openpilot.selfdrive.ui.sunnypilot.onroad.augmented_road_view import BORDER_COLORS_SP, AugmentedRoadViewSP from openpilot.selfdrive.ui.sunnypilot.onroad.driver_state import DriverStateRendererSP as DriverStateRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.hud_renderer import HudRendererSP as HudRenderer from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus @@ -38,9 +38,10 @@ ROAD_CAM_MIN_SPEED = 15.0 # m/s (34 mph) INF_POINT = np.array([1000.0, 0.0, 0.0]) -class AugmentedRoadView(CameraView): +class AugmentedRoadView(CameraView, AugmentedRoadViewSP): def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD): - super().__init__("camerad", stream_type) + CameraView.__init__(self, "camerad", stream_type) + AugmentedRoadViewSP.__init__(self) self._set_placeholder_color(BORDER_COLORS[UIStatus.DISENGAGED]) self.device_camera: DeviceCameraConfig | None = None @@ -92,6 +93,7 @@ class AugmentedRoadView(CameraView): # Draw all UI overlays self.model_renderer.render(self._content_rect) + AugmentedRoadViewSP.update_fade_out_bottom_overlay(self, self._content_rect) self._hud_renderer.render(self._content_rect) self.alert_renderer.render(self._content_rect) self.driver_state_renderer.render(self._content_rect) diff --git a/selfdrive/ui/sunnypilot/onroad/augmented_road_view.py b/selfdrive/ui/sunnypilot/onroad/augmented_road_view.py index 0a5739cc00..c7dedee540 100644 --- a/selfdrive/ui/sunnypilot/onroad/augmented_road_view.py +++ b/selfdrive/ui/sunnypilot/onroad/augmented_road_view.py @@ -5,9 +5,27 @@ 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 pyray as rl -from openpilot.selfdrive.ui.ui_state import UIStatus +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.selfdrive.ui.ui_state import UIStatus, ui_state +from openpilot.system.ui.lib.application import gui_app BORDER_COLORS_SP = { UIStatus.LAT_ONLY: rl.Color(0x00, 0xC8, 0xC8, 0xFF), # Cyan for lateral-only state UIStatus.LONG_ONLY: rl.Color(0x96, 0x1C, 0xA8, 0xFF), # Purple for longitudinal-only state } + + +class AugmentedRoadViewSP: + def __init__(self): + self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png") + self._fade_alpha_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) + + def update_fade_out_bottom_overlay(self, _content_rect): + # Fade out bottom of overlays for looks (only when engaged) + fade_alpha = self._fade_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED) + if ui_state.torque_bar and fade_alpha > 1e-2: + # Scale the fade texture to the content rect + rl.draw_texture_pro(self._fade_texture, + rl.Rectangle(0, 0, self._fade_texture.width, self._fade_texture.height), + _content_rect, rl.Vector2(0, 0), 0.0, + rl.Color(255, 255, 255, int(255 * fade_alpha))) diff --git a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py index 8ca7269802..74e15fe0bd 100644 --- a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py +++ b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py @@ -6,6 +6,7 @@ See the LICENSE.md file in the root directory for more details. """ import pyray as rl +from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer @@ -23,6 +24,7 @@ class HudRendererSP(HudRenderer): self.rocket_fuel = RocketFuel() self.speed_limit_renderer = SpeedLimitRenderer() self.turn_signal_controller = TurnSignalController() + self._torque_bar = TorqueBar(scale=3.0, always=True) def _update_state(self) -> None: super()._update_state() @@ -32,6 +34,13 @@ class HudRendererSP(HudRenderer): def _render(self, rect: rl.Rectangle) -> None: super()._render(rect) + + if ui_state.torque_bar and ui_state.sm['controlsState'].lateralControlState.which() != 'angleState': + torque_rect = rect + if ui_state.developer_ui in (DeveloperUiRenderer.DEV_UI_BOTTOM, DeveloperUiRenderer.DEV_UI_BOTH): + torque_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height - DeveloperUiRenderer.BOTTOM_BAR_HEIGHT) + self._torque_bar.render(torque_rect) + self.developer_ui.render(rect) self.road_name_renderer.render(rect) self.speed_limit_renderer.render(rect) diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index f38280d498..98ddbe5266 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -125,6 +125,7 @@ class UIStateSP: self.rocket_fuel = self.params.get_bool("RocketFuel") self.rainbow_path = self.params.get_bool("RainbowMode") self.chevron_metrics = self.params.get("ChevronInfo") + self.torque_bar = self.params.get_bool("TorqueBar") self.active_bundle = self.params.get("ModelManager_ActiveBundle") self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True) self.speed_limit_mode = self.params.get("SpeedLimitMode", return_default=True) diff --git a/sunnypilot/sunnylink/params_metadata.json b/sunnypilot/sunnylink/params_metadata.json index 7f597a9621..090beb49f0 100644 --- a/sunnypilot/sunnylink/params_metadata.json +++ b/sunnypilot/sunnylink/params_metadata.json @@ -1239,6 +1239,10 @@ "title": "Tesla Coop Steering", "description": "" }, + "TorqueBar": { + "title": "Steering Arc", + "description": "[TIZI/TICI only] Display steering arc on the driving screen when lateral control is enabled." + }, "TorqueParamsOverrideEnabled": { "title": "Manual Real-Time Tuning", "description": "" From c274dba36ed91f82eb1ad8cddafe9de2ea09c0c7 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 8 Feb 2026 18:28:36 -0500 Subject: [PATCH 05/11] [TIZI/TICI] ui: Smart Cruise Control elements (#1675) * init * punch * lower * colors --- .../ui/sunnypilot/onroad/hud_renderer.py | 4 + .../sunnypilot/onroad/smart_cruise_control.py | 131 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py diff --git a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py index 74e15fe0bd..d6f9278d22 100644 --- a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py +++ b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py @@ -13,6 +13,7 @@ from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRen from openpilot.selfdrive.ui.sunnypilot.onroad.road_name import RoadNameRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.rocket_fuel import RocketFuel from openpilot.selfdrive.ui.sunnypilot.onroad.speed_limit import SpeedLimitRenderer +from openpilot.selfdrive.ui.sunnypilot.onroad.smart_cruise_control import SmartCruiseControlRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.turn_signal import TurnSignalController @@ -23,6 +24,7 @@ class HudRendererSP(HudRenderer): self.road_name_renderer = RoadNameRenderer() self.rocket_fuel = RocketFuel() self.speed_limit_renderer = SpeedLimitRenderer() + self.smart_cruise_control_renderer = SmartCruiseControlRenderer() self.turn_signal_controller = TurnSignalController() self._torque_bar = TorqueBar(scale=3.0, always=True) @@ -30,6 +32,7 @@ class HudRendererSP(HudRenderer): super()._update_state() self.road_name_renderer.update() self.speed_limit_renderer.update() + self.smart_cruise_control_renderer.update() self.turn_signal_controller.update() def _render(self, rect: rl.Rectangle) -> None: @@ -44,6 +47,7 @@ class HudRendererSP(HudRenderer): self.developer_ui.render(rect) self.road_name_renderer.render(rect) self.speed_limit_renderer.render(rect) + self.smart_cruise_control_renderer.render(rect) self.turn_signal_controller.render(rect) if ui_state.rocket_fuel: diff --git a/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py b/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py new file mode 100644 index 0000000000..ca71fcac4a --- /dev/null +++ b/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py @@ -0,0 +1,131 @@ +""" +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 pyray as rl + +from openpilot.selfdrive.ui.onroad.hud_renderer import COLORS +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets import Widget + + +class SmartCruiseControlRenderer(Widget): + def __init__(self): + super().__init__() + self.vision_enabled = False + self.vision_active = False + self.vision_frame = 0 + self.map_enabled = False + self.map_active = False + self.map_frame = 0 + self.long_override = False + + self.font = gui_app.font(FontWeight.BOLD) + self.scc_tex = rl.load_render_texture(256, 128) + + def update(self): + sm = ui_state.sm + if sm.updated["longitudinalPlanSP"]: + lp_sp = sm["longitudinalPlanSP"] + vision = lp_sp.smartCruiseControl.vision + map_ = lp_sp.smartCruiseControl.map + + self.vision_enabled = vision.enabled + self.vision_active = vision.active + self.map_enabled = map_.enabled + self.map_active = map_.active + + if sm.updated["carControl"]: + self.long_override = sm["carControl"].cruiseControl.override + + if self.vision_active: + self.vision_frame += 1 + else: + self.vision_frame = 0 + + if self.map_active: + self.map_frame += 1 + else: + self.map_frame = 0 + + @staticmethod + def _pulse_element(frame): + return not (frame % gui_app.target_fps < (gui_app.target_fps / 2.5)) + + def _draw_icon(self, rect_center_x, rect_height, x_offset, y_offset, name): + text = name + font_size = 36 + padding_v = 5 + box_width = 160 + + sz = measure_text_cached(self.font, text, font_size) + box_height = int(sz.y + padding_v * 2) + + texture_width = 256 + texture_height = 128 + + rl.begin_texture_mode(self.scc_tex) + rl.clear_background(rl.Color(0, 0, 0, 0)) + + if self.long_override: + box_color = COLORS.OVERRIDE + else: + box_color = rl.Color(0, 255, 0, 255) + + # Center box in texture + box_x = (texture_width - box_width) // 2 + box_y = (texture_height - box_height) // 2 + + rl.draw_rectangle_rounded(rl.Rectangle(box_x, box_y, box_width, box_height), 0.2, 10, box_color) + + # Draw text with custom blend mode to punch hole + rl.rl_set_blend_factors(rl.RL_ZERO, rl.RL_ONE_MINUS_SRC_ALPHA, 0x8006) + rl.rl_set_blend_mode(rl.BLEND_CUSTOM) + + text_pos_x = box_x + (box_width - sz.x) / 2 + text_pos_y = box_y + (box_height - sz.y) / 2 + + rl.draw_text_ex(self.font, text, rl.Vector2(text_pos_x, text_pos_y), font_size, 0, rl.WHITE) + + rl.rl_set_blend_mode(rl.BLEND_ALPHA) # Reset + rl.end_texture_mode() + + screen_y = rect_height / 4 + y_offset + + dest_x = rect_center_x + x_offset - texture_width / 2 + dest_y = screen_y - texture_height / 2 + + src_rect = rl.Rectangle(0, 0, texture_width, -texture_height) + dst_rect = rl.Rectangle(dest_x, dest_y, texture_width, texture_height) + + rl.draw_texture_pro(self.scc_tex.texture, src_rect, dst_rect, rl.Vector2(0, 0), 0, rl.WHITE) + + def _render(self, rect: rl.Rectangle): + x_offset = -260 + y1_offset = -40 + y2_offset = -100 + + orders = [y1_offset, y2_offset] + y_scc_v = 0 + y_scc_m = 0 + idx = 0 + + if self.vision_enabled: + y_scc_v = orders[idx] + idx += 1 + + if self.map_enabled: + y_scc_m = orders[idx] + idx += 1 + + scc_vision_pulse = self._pulse_element(self.vision_frame) + if (self.vision_enabled and not self.vision_active) or (self.vision_active and scc_vision_pulse): + self._draw_icon(rect.x + rect.width / 2, rect.height, x_offset, y_scc_v, "SCC-V") + + scc_map_pulse = self._pulse_element(self.map_frame) + if (self.map_enabled and not self.map_active) or (self.map_active and scc_map_pulse): + self._draw_icon(rect.x + rect.width / 2, rect.height, x_offset, y_scc_m, "SCC-M") From 020f503364c3a4380de23175aace486f04d88828 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 8 Feb 2026 19:14:19 -0500 Subject: [PATCH 06/11] [TIZI/TICI] ui: Green Light and Lead Departure elements (#1676) * init * big for now * only adjust for right dev ui for now * final * final final --- selfdrive/ui/sunnypilot/onroad/e2e_alerts.py | 101 ++++++++++++++++++ .../ui/sunnypilot/onroad/hud_renderer.py | 4 + 2 files changed, 105 insertions(+) create mode 100644 selfdrive/ui/sunnypilot/onroad/e2e_alerts.py diff --git a/selfdrive/ui/sunnypilot/onroad/e2e_alerts.py b/selfdrive/ui/sunnypilot/onroad/e2e_alerts.py new file mode 100644 index 0000000000..b15db94c16 --- /dev/null +++ b/selfdrive/ui/sunnypilot/onroad/e2e_alerts.py @@ -0,0 +1,101 @@ +""" +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 pyray as rl + +from cereal import log +from openpilot.selfdrive.ui import UI_BORDER_SIZE +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer +from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE +from openpilot.system.ui.lib.text_measure import measure_text_cached + + +class E2eAlertsRenderer: + def __init__(self): + self._green_light_alert_img = gui_app.texture("../../sunnypilot/selfdrive/assets/images/green_light.png", 250, 250) + self._lead_depart_alert_img = gui_app.texture("../../sunnypilot/selfdrive/assets/images/lead_depart.png", 250, 250) + + self._e2e_alert_display_timer = 0 + self._e2e_alert_frame = 0 + self._green_light_alert = False + self._lead_depart_alert = False + self._alert_text = "" + self._alert_img = None + self._allow_e2e_alerts = False + + def update(self) -> None: + sm = ui_state.sm + lp_sp = sm['longitudinalPlanSP'] + self._green_light_alert = lp_sp.e2eAlerts.greenLightAlert + self._lead_depart_alert = lp_sp.e2eAlerts.leadDepartAlert + + self._allow_e2e_alerts = sm['selfdriveState'].alertSize == log.SelfdriveState.AlertSize.none and \ + sm.recv_frame['driverStateV2'] > ui_state.started_frame + + if self._green_light_alert or self._lead_depart_alert: + self._e2e_alert_display_timer = 3 * gui_app.target_fps + + if self._e2e_alert_display_timer > 0: + self._e2e_alert_frame += 1 + self._e2e_alert_display_timer -= 1 + + if self._green_light_alert: + self._alert_text = "GREEN\nLIGHT" + self._alert_img = self._green_light_alert_img + elif self._lead_depart_alert: + self._alert_text = "LEAD VEHICLE\nDEPARTING" + self._alert_img = self._lead_depart_alert_img + else: + self._e2e_alert_frame = 0 + + def render(self, rect: rl.Rectangle) -> None: + if not self._allow_e2e_alerts or self._e2e_alert_display_timer <= 0: + return + + e2e_alert_size = 250 + dev_ui_width_adjustment = 180 if ui_state.developer_ui in (DeveloperUiRenderer.DEV_UI_RIGHT, DeveloperUiRenderer.DEV_UI_BOTH) else 100 + + x = rect.x + rect.width - e2e_alert_size - dev_ui_width_adjustment - (UI_BORDER_SIZE * 3) + y = rect.y + rect.height / 2 + 20 + + alert_rect = rl.Rectangle(x - e2e_alert_size, y - e2e_alert_size, e2e_alert_size * 2, e2e_alert_size * 2) + center = rl.Vector2(alert_rect.x + alert_rect.width / 2, alert_rect.y + alert_rect.height / 2) + + # Pulse logic + is_pulsing = (self._e2e_alert_frame % gui_app.target_fps) < (gui_app.target_fps / 2.5) + frame_color = rl.Color(255, 255, 255, 75) if is_pulsing else rl.Color(0, 255, 0, 75) + + # Draw Circle + rl.draw_circle_v(center, e2e_alert_size, rl.Color(0, 0, 0, 190)) + # Draw Ring (Border) + rl.draw_ring(center, e2e_alert_size - 7.5, e2e_alert_size + 7.5, 0, 360, 0, frame_color) + + # Draw Image + if self._alert_img: + img_x = int(center.x - self._alert_img.width / 2) + img_y = int(center.y - self._alert_img.height / 2) + rl.draw_texture(self._alert_img, img_x, img_y, rl.WHITE) + + # Draw Text + txt_color = rl.Color(255, 255, 255, 255) if is_pulsing else rl.Color(0, 255, 0, 190) + font = gui_app.font(FontWeight.BOLD) + text_size = 48 + spacing = 0 + + lines = self._alert_text.split('\n') + + # Position text at bottom of alert circle + bottom_y = (alert_rect.y + alert_rect.height) - (alert_rect.height / 7) + + # Draw lines upwards from bottom + current_y = bottom_y - (len(lines) * text_size * FONT_SCALE) + + for line in lines: + measure = measure_text_cached(font, line, text_size, spacing) + line_x = center.x - measure.x / 2 + rl.draw_text_ex(font, line, rl.Vector2(line_x, current_y), text_size, spacing, txt_color) + current_y += text_size * FONT_SCALE diff --git a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py index d6f9278d22..d99e3cb0d1 100644 --- a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py +++ b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py @@ -15,6 +15,7 @@ from openpilot.selfdrive.ui.sunnypilot.onroad.rocket_fuel import RocketFuel from openpilot.selfdrive.ui.sunnypilot.onroad.speed_limit import SpeedLimitRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.smart_cruise_control import SmartCruiseControlRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.turn_signal import TurnSignalController +from openpilot.selfdrive.ui.sunnypilot.onroad.e2e_alerts import E2eAlertsRenderer class HudRendererSP(HudRenderer): @@ -26,6 +27,7 @@ class HudRendererSP(HudRenderer): self.speed_limit_renderer = SpeedLimitRenderer() self.smart_cruise_control_renderer = SmartCruiseControlRenderer() self.turn_signal_controller = TurnSignalController() + self.e2e_alerts_renderer = E2eAlertsRenderer() self._torque_bar = TorqueBar(scale=3.0, always=True) def _update_state(self) -> None: @@ -34,6 +36,7 @@ class HudRendererSP(HudRenderer): self.speed_limit_renderer.update() self.smart_cruise_control_renderer.update() self.turn_signal_controller.update() + self.e2e_alerts_renderer.update() def _render(self, rect: rl.Rectangle) -> None: super()._render(rect) @@ -49,6 +52,7 @@ class HudRendererSP(HudRenderer): self.speed_limit_renderer.render(rect) self.smart_cruise_control_renderer.render(rect) self.turn_signal_controller.render(rect) + self.e2e_alerts_renderer.render(rect) if ui_state.rocket_fuel: self.rocket_fuel.render(rect, ui_state.sm) From a9229e11a067ef75c64565f4065b58f6bb38e404 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 8 Feb 2026 19:40:04 -0500 Subject: [PATCH 07/11] [TIZI/TICI] ui: standstill timer (#1677) * standstill timer * final --- .../{e2e_alerts.py => circular_alerts.py} | 59 ++++++++++++++++--- .../ui/sunnypilot/onroad/hud_renderer.py | 8 +-- selfdrive/ui/sunnypilot/ui_state.py | 1 + 3 files changed, 55 insertions(+), 13 deletions(-) rename selfdrive/ui/sunnypilot/onroad/{e2e_alerts.py => circular_alerts.py} (59%) diff --git a/selfdrive/ui/sunnypilot/onroad/e2e_alerts.py b/selfdrive/ui/sunnypilot/onroad/circular_alerts.py similarity index 59% rename from selfdrive/ui/sunnypilot/onroad/e2e_alerts.py rename to selfdrive/ui/sunnypilot/onroad/circular_alerts.py index b15db94c16..965ce7fe77 100644 --- a/selfdrive/ui/sunnypilot/onroad/e2e_alerts.py +++ b/selfdrive/ui/sunnypilot/onroad/circular_alerts.py @@ -14,7 +14,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.text_measure import measure_text_cached -class E2eAlertsRenderer: +class CircularAlertsRenderer: def __init__(self): self._green_light_alert_img = gui_app.texture("../../sunnypilot/selfdrive/assets/images/green_light.png", 250, 250) self._lead_depart_alert_img = gui_app.texture("../../sunnypilot/selfdrive/assets/images/lead_depart.png", 250, 250) @@ -23,6 +23,9 @@ class E2eAlertsRenderer: self._e2e_alert_frame = 0 self._green_light_alert = False self._lead_depart_alert = False + self._standstill_timer = False + self._standstill_elapsed_time = 0.0 + self._is_standstill = False self._alert_text = "" self._alert_img = None self._allow_e2e_alerts = False @@ -30,14 +33,22 @@ class E2eAlertsRenderer: def update(self) -> None: sm = ui_state.sm lp_sp = sm['longitudinalPlanSP'] + car_state = sm['carState'] self._green_light_alert = lp_sp.e2eAlerts.greenLightAlert self._lead_depart_alert = lp_sp.e2eAlerts.leadDepartAlert + self._standstill_timer = ui_state.standstill_timer + self._is_standstill = car_state.standstill + + if not ui_state.started: + self._standstill_elapsed_time = 0.0 self._allow_e2e_alerts = sm['selfdriveState'].alertSize == log.SelfdriveState.AlertSize.none and \ sm.recv_frame['driverStateV2'] > ui_state.started_frame if self._green_light_alert or self._lead_depart_alert: self._e2e_alert_display_timer = 3 * gui_app.target_fps + # reset onroad sleep timer for e2e alerts + ui_state.reset_onroad_sleep_timer() if self._e2e_alert_display_timer > 0: self._e2e_alert_frame += 1 @@ -49,11 +60,22 @@ class E2eAlertsRenderer: elif self._lead_depart_alert: self._alert_text = "LEAD VEHICLE\nDEPARTING" self._alert_img = self._lead_depart_alert_img + + elif self._standstill_timer and self._is_standstill: + self._alert_img = None + self._standstill_elapsed_time += 1.0 / gui_app.target_fps + minute = int(self._standstill_elapsed_time / 60) + second = int(self._standstill_elapsed_time - (minute * 60)) + self._alert_text = f"{minute:01d}:{second:02d}" + self._e2e_alert_frame += 1 + else: self._e2e_alert_frame = 0 + if not self._is_standstill: + self._standstill_elapsed_time = 0.0 def render(self, rect: rl.Rectangle) -> None: - if not self._allow_e2e_alerts or self._e2e_alert_display_timer <= 0: + if not self._allow_e2e_alerts or (self._e2e_alert_display_timer <= 0 and not (self._standstill_timer and self._is_standstill)): return e2e_alert_size = 250 @@ -67,7 +89,12 @@ class E2eAlertsRenderer: # Pulse logic is_pulsing = (self._e2e_alert_frame % gui_app.target_fps) < (gui_app.target_fps / 2.5) - frame_color = rl.Color(255, 255, 255, 75) if is_pulsing else rl.Color(0, 255, 0, 75) + + # Standstill Timer (STOPPED) should be static white + if self._e2e_alert_display_timer == 0 and self._standstill_timer and self._is_standstill: + frame_color = rl.Color(255, 255, 255, 75) + else: + frame_color = rl.Color(255, 255, 255, 75) if is_pulsing else rl.Color(0, 255, 0, 75) # Draw Circle rl.draw_circle_v(center, e2e_alert_size, rl.Color(0, 0, 0, 190)) @@ -75,7 +102,7 @@ class E2eAlertsRenderer: rl.draw_ring(center, e2e_alert_size - 7.5, e2e_alert_size + 7.5, 0, 360, 0, frame_color) # Draw Image - if self._alert_img: + if self._alert_img and self._e2e_alert_display_timer > 0: img_x = int(center.x - self._alert_img.width / 2) img_y = int(center.y - self._alert_img.height / 2) rl.draw_texture(self._alert_img, img_x, img_y, rl.WHITE) @@ -94,8 +121,22 @@ class E2eAlertsRenderer: # Draw lines upwards from bottom current_y = bottom_y - (len(lines) * text_size * FONT_SCALE) - for line in lines: - measure = measure_text_cached(font, line, text_size, spacing) - line_x = center.x - measure.x / 2 - rl.draw_text_ex(font, line, rl.Vector2(line_x, current_y), text_size, spacing, txt_color) - current_y += text_size * FONT_SCALE + if self._e2e_alert_display_timer == 0 and self._standstill_timer and self._is_standstill: + # Standstill Timer Text + alert_alt_text = "STOPPED" + top_text_size = 80 + measure_top = measure_text_cached(font, alert_alt_text, top_text_size, spacing) + top_y = alert_rect.y + alert_rect.height / 3.5 + rl.draw_text_ex(font, alert_alt_text, rl.Vector2(center.x - measure_top.x / 2, top_y), top_text_size, spacing, rl.Color(255, 175, 3, 240)) + + # Timer + timer_text_size = 100 + measure_timer = measure_text_cached(font, self._alert_text, timer_text_size, spacing) + timer_y = (alert_rect.y + alert_rect.height) - (alert_rect.height / 5) - measure_timer.y + rl.draw_text_ex(font, self._alert_text, rl.Vector2(center.x - measure_timer.x / 2, timer_y), timer_text_size, spacing, rl.WHITE) + else: + for line in lines: + measure = measure_text_cached(font, line, text_size, spacing) + line_x = center.x - measure.x / 2 + rl.draw_text_ex(font, line, rl.Vector2(line_x, current_y), text_size, spacing, txt_color) + current_y += text_size * FONT_SCALE diff --git a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py index d99e3cb0d1..f765936d6e 100644 --- a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py +++ b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py @@ -15,7 +15,7 @@ from openpilot.selfdrive.ui.sunnypilot.onroad.rocket_fuel import RocketFuel from openpilot.selfdrive.ui.sunnypilot.onroad.speed_limit import SpeedLimitRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.smart_cruise_control import SmartCruiseControlRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.turn_signal import TurnSignalController -from openpilot.selfdrive.ui.sunnypilot.onroad.e2e_alerts import E2eAlertsRenderer +from openpilot.selfdrive.ui.sunnypilot.onroad.circular_alerts import CircularAlertsRenderer class HudRendererSP(HudRenderer): @@ -27,7 +27,7 @@ class HudRendererSP(HudRenderer): self.speed_limit_renderer = SpeedLimitRenderer() self.smart_cruise_control_renderer = SmartCruiseControlRenderer() self.turn_signal_controller = TurnSignalController() - self.e2e_alerts_renderer = E2eAlertsRenderer() + self.circular_alerts_renderer = CircularAlertsRenderer() self._torque_bar = TorqueBar(scale=3.0, always=True) def _update_state(self) -> None: @@ -36,7 +36,7 @@ class HudRendererSP(HudRenderer): self.speed_limit_renderer.update() self.smart_cruise_control_renderer.update() self.turn_signal_controller.update() - self.e2e_alerts_renderer.update() + self.circular_alerts_renderer.update() def _render(self, rect: rl.Rectangle) -> None: super()._render(rect) @@ -52,7 +52,7 @@ class HudRendererSP(HudRenderer): self.speed_limit_renderer.render(rect) self.smart_cruise_control_renderer.render(rect) self.turn_signal_controller.render(rect) - self.e2e_alerts_renderer.render(rect) + self.circular_alerts_renderer.render(rect) if ui_state.rocket_fuel: self.rocket_fuel.render(rect, ui_state.sm) diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 98ddbe5266..89a650bae9 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -129,6 +129,7 @@ class UIStateSP: self.active_bundle = self.params.get("ModelManager_ActiveBundle") self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True) self.speed_limit_mode = self.params.get("SpeedLimitMode", return_default=True) + self.standstill_timer = self.params.get_bool("StandstillTimer") # Onroad Screen Brightness self.onroad_brightness = int(float(self.params.get("OnroadScreenOffBrightness", return_default=True))) From 96b58024abcb25eed9d2e0a7a1917cd2fadaeff9 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Sun, 8 Feb 2026 16:54:33 -0800 Subject: [PATCH 08/11] [MICI] ui: driving models selector (#1574) * ui: models mici * Update models.py * Update models.py * sync --------- Co-authored-by: Jason Wen --- .../ui/sunnypilot/mici/layouts/models.py | 122 ++++++++++++++++++ .../ui/sunnypilot/mici/layouts/settings.py | 8 ++ 2 files changed, 130 insertions(+) create mode 100644 selfdrive/ui/sunnypilot/mici/layouts/models.py diff --git a/selfdrive/ui/sunnypilot/mici/layouts/models.py b/selfdrive/ui/sunnypilot/mici/layouts/models.py new file mode 100644 index 0000000000..d5964ea964 --- /dev/null +++ b/selfdrive/ui/sunnypilot/mici/layouts/models.py @@ -0,0 +1,122 @@ +""" +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. +""" +from collections.abc import Callable + +from cereal import custom +from openpilot.selfdrive.ui.mici.widgets.button import BigButton +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.widgets import NavWidget, Widget +from openpilot.system.ui.widgets.scroller import Scroller + + +class ModelsLayoutMici(NavWidget): + def __init__(self, back_callback: Callable): + super().__init__() + self.set_back_callback(back_callback) + self.original_back_callback = back_callback + self.focused_widget = None + + self.current_model_btn = BigButton(tr("current model"), "", "") + self.current_model_btn.set_click_callback(self._show_folders) + + self.cancel_download_btn = BigButton(tr("cancel download"), "", "") + self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadIndex")) + + self.main_items: list[Widget] = [self.current_model_btn, self.cancel_download_btn] + self._scroller = Scroller(self.main_items, snap_items=False) + + @property + def model_manager(self): + return ui_state.sm["modelManagerSP"] + + def _get_grouped_bundles(self): + bundles = self.model_manager.availableBundles + folders = {} + for bundle in bundles: + folder = next((override.value for override in bundle.overrides if override.key == "folder"), "") + folders.setdefault(folder, []).append(bundle) + return folders + + def _show_selection_view(self, items: list[Widget], back_callback: Callable): + self._scroller._items = items + for item in items: + item.set_touch_valid_callback(lambda: self._scroller.scroll_panel.is_touch_valid() and self._scroller.enabled) + self._scroller.scroll_panel.set_offset(0) + self.set_back_callback(back_callback) + + def _show_folders(self): + self.focused_widget = self.current_model_btn + folders = self._get_grouped_bundles() + folder_buttons = [] + default_btn = BigButton(tr("default model"), "", "") + default_btn.set_click_callback(self._select_default) + folder_buttons.append(default_btn) + + for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True): + if folder.lower() in ["release models", "master models"]: + btn = BigButton(folder.lower(), "", "") + btn.set_click_callback(lambda f=folder: self._select_folder(f)) + folder_buttons.append(btn) + self._show_selection_view(folder_buttons, self._reset_main_view) + + def _select_model(self, bundle): + ui_state.params.put("ModelManager_DownloadIndex", bundle.index) + self._reset_main_view() + + def _select_default(self): + ui_state.params.remove("ModelManager_ActiveBundle") + self._reset_main_view() + + def _select_folder(self, folder_name): + folders = self._get_grouped_bundles() + bundles = sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True) + + btns = [] + for bundle in bundles: + txt = bundle.displayName.lower() + btn = BigButton(txt, "", "") + btn.set_click_callback(lambda b=bundle: self._select_model(b)) + btns.append(btn) + self._show_selection_view(btns, self._show_folders) + + def _reset_main_view(self): + self._scroller._items = self.main_items + self.set_back_callback(self.original_back_callback) + if self.focused_widget and self.focused_widget in self.main_items: + x = self._scroller._pad_start + for item in self.main_items: + if not item.is_visible: + continue + if item == self.focused_widget: + break + x += item.rect.width + self._scroller._spacing + self._scroller.scroll_panel.set_offset(0) + self._scroller.scroll_to(x) + self.focused_widget = None + else: + self._scroller.scroll_panel.set_offset(0) + + def _update_state(self): + super()._update_state() + + manager = self.model_manager + if manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading: + self.current_model_btn.set_value("downloading...") + self.cancel_download_btn.set_visible(True) + else: + self.current_model_btn.set_value(manager.activeBundle.internalName.lower() if manager.activeBundle else tr("default model")) + self.cancel_download_btn.set_visible(False) + self.current_model_btn.set_enabled(ui_state.is_offroad()) + self.current_model_btn.set_text(tr("current model")) + + def _render(self, rect): + self._scroller.render(rect) + + def show_event(self): + super().show_event() + self._scroller.show_event() diff --git a/selfdrive/ui/sunnypilot/mici/layouts/settings.py b/selfdrive/ui/sunnypilot/mici/layouts/settings.py index f6fae6630c..69982e2298 100644 --- a/selfdrive/ui/sunnypilot/mici/layouts/settings.py +++ b/selfdrive/ui/sunnypilot/mici/layouts/settings.py @@ -9,6 +9,7 @@ from enum import IntEnum from openpilot.selfdrive.ui.mici.layouts.settings import settings as OP from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici +from openpilot.selfdrive.ui.sunnypilot.mici.layouts.models import ModelsLayoutMici ICON_SIZE = 70 @@ -16,6 +17,7 @@ OP.PanelType = IntEnum( "PanelType", [es.name for es in OP.PanelType] + [ "SUNNYLINK", + "MODELS", ], start=0, ) @@ -27,13 +29,19 @@ class SettingsLayoutSP(OP.SettingsLayout): sunnylink_btn = BigButton("sunnylink", "", "icons_mici/settings/developer/ssh.png") sunnylink_btn.set_click_callback(lambda: self._set_current_panel(OP.PanelType.SUNNYLINK)) + + models_btn = BigButton("models", "", "../../sunnypilot/selfdrive/assets/offroad/icon_models.png") + models_btn.set_click_callback(lambda: self._set_current_panel(OP.PanelType.MODELS)) + self._panels.update({ OP.PanelType.SUNNYLINK: OP.PanelInfo("sunnylink", SunnylinkLayoutMici(back_callback=lambda: self._set_current_panel(None))), + OP.PanelType.MODELS: OP.PanelInfo("models", ModelsLayoutMici(back_callback=lambda: self._set_current_panel(None))), }) items = self._scroller._items.copy() items.insert(1, sunnylink_btn) + items.insert(2, models_btn) self._scroller._items.clear() for item in items: self._scroller.add_widget(item) From 254f55ac15a40343d7255f2f098de3442e0c4a6f Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 8 Feb 2026 20:42:23 -0500 Subject: [PATCH 09/11] [TIZI/TICI] ui: Hide vEgo and True vEgo (#1678) --- .../ui/sunnypilot/onroad/hud_renderer.py | 6 +++ .../ui/sunnypilot/onroad/speed_renderer.py | 46 +++++++++++++++++++ selfdrive/ui/sunnypilot/ui_state.py | 2 + 3 files changed, 54 insertions(+) create mode 100644 selfdrive/ui/sunnypilot/onroad/speed_renderer.py diff --git a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py index f765936d6e..3b810d62e9 100644 --- a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py +++ b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py @@ -16,6 +16,7 @@ from openpilot.selfdrive.ui.sunnypilot.onroad.speed_limit import SpeedLimitRende from openpilot.selfdrive.ui.sunnypilot.onroad.smart_cruise_control import SmartCruiseControlRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.turn_signal import TurnSignalController from openpilot.selfdrive.ui.sunnypilot.onroad.circular_alerts import CircularAlertsRenderer +from openpilot.selfdrive.ui.sunnypilot.onroad.speed_renderer import SpeedRenderer class HudRendererSP(HudRenderer): @@ -28,6 +29,7 @@ class HudRendererSP(HudRenderer): self.smart_cruise_control_renderer = SmartCruiseControlRenderer() self.turn_signal_controller = TurnSignalController() self.circular_alerts_renderer = CircularAlertsRenderer() + self.speed_renderer = SpeedRenderer() self._torque_bar = TorqueBar(scale=3.0, always=True) def _update_state(self) -> None: @@ -37,6 +39,10 @@ class HudRendererSP(HudRenderer): self.smart_cruise_control_renderer.update() self.turn_signal_controller.update() self.circular_alerts_renderer.update() + self.speed_renderer.update() + + def _draw_current_speed(self, rect: rl.Rectangle) -> None: + self.speed_renderer.render(rect) def _render(self, rect: rl.Rectangle) -> None: super()._render(rect) diff --git a/selfdrive/ui/sunnypilot/onroad/speed_renderer.py b/selfdrive/ui/sunnypilot/onroad/speed_renderer.py new file mode 100644 index 0000000000..0a017876e1 --- /dev/null +++ b/selfdrive/ui/sunnypilot/onroad/speed_renderer.py @@ -0,0 +1,46 @@ +""" +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 pyray as rl + +from openpilot.common.constants import CV +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.selfdrive.ui.onroad.hud_renderer import FONT_SIZES, COLORS + + +class SpeedRenderer: + def __init__(self): + self.speed: float = 0.0 + self.v_ego_cluster_seen: bool = False + + self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD) + self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM) + + def update(self) -> None: + car_state = ui_state.sm['carState'] + v_ego_cluster = car_state.vEgoCluster + self.v_ego_cluster_seen = self.v_ego_cluster_seen or v_ego_cluster != 0.0 + v_ego = v_ego_cluster if self.v_ego_cluster_seen and not ui_state.true_v_ego_ui else car_state.vEgo + speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH + self.speed = max(0.0, v_ego * speed_conversion) + + def render(self, rect: rl.Rectangle) -> None: + if ui_state.hide_v_ego_ui: + return + + # Draw current speed and unit + speed_text = str(round(self.speed)) + speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed) + speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2) + rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.WHITE) + + unit_text = tr("km/h") if ui_state.is_metric else tr("mph") + unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit) + unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2) + rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.WHITE_TRANSLUCENT) diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 89a650bae9..6d7eab51cd 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -130,6 +130,8 @@ class UIStateSP: self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True) self.speed_limit_mode = self.params.get("SpeedLimitMode", return_default=True) self.standstill_timer = self.params.get_bool("StandstillTimer") + self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI") + self.hide_v_ego_ui = self.params.get_bool("HideVEgoUI") # Onroad Screen Brightness self.onroad_brightness = int(float(self.params.get("OnroadScreenOffBrightness", return_default=True))) From 981494a35433e4edbef7b38ea6676a9cdafe121b Mon Sep 17 00:00:00 2001 From: Nayan Date: Mon, 9 Feb 2026 00:17:34 -0500 Subject: [PATCH 10/11] [TIZI/TICI] ui: Visuals panel (#1496) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * commaai/openpilot:d05cb31e2e916fba41ba8167030945f427fd811b * bump opendbc * bump opendbc * bump opendbc * bump opendbc * bump opendbc * sunnypilot: remove Qt * cabana: revert to stock Qt * commaai/openpilot:5198b1b079c37742c1050f02ce0aa6dd42b038b9 * commaai/openpilot:954b567b9ba0f3d1ae57d6aa7797fa86dd92ec6e * commaai/openpilot:7534b2a160faa683412c04c1254440e338931c5e * sum more * bump opendbc * not yet * should've been symlink'ed * raylib says wut * quiet mode back * more fixes * no more * too extra red diff on the side * need to bring this back * too extra * let's update docs here * Revert "let's update docs here" This reverts commit 51fe03cd5121e6fdf14657b2c33852c34922b851. * param to control stock vs sp ui * init styles * SP Toggles * Lint * optimizations * multi-button * Lint * param to control stock vs sp ui * init styles * SP Toggles * Lint * optimizations * Panels. With Icons. And Scroller. * patience, grasshopper * more patience, grasshopper * sp raylib preview * fix callback * fix ui preview * add ui previews * introducing ui_state_sp for py * param to control stock vs sp ui * better * add ui_update callback * better padding * init * revert padding to 20 * new line, who dis * this * support for next line multi-button * use inline=false * uhh * disabled colors * hide em all * lambdas * NOT inline * final touches * hide HIDE * ruff.. RUFF.. WHY RUFF * listitem -> listitemsp * Revert "add ui_update callback" This reverts commit 4da32cc0097434aab0aa6a3c35465eabb23c8958. * add show_description method * remove padding from line separator. like, WHY? 😩😩 * scroller -> scroller_tici * scroller -> scroller_tici * remove line separator padding * ui: `GuiApplicationExt` * add to readme * use gui_app.sunnypilot_ui() * use gui_app.sunnypilot_ui() * use gui_app.sunnypilot_ui() * uhhh. nope * optimizations * I THINK this is not needed, i don't see it used on the visuals panel... * unhide for now... Why hidden tho? * refresh controls * missing * blindspot * standstill timer * road name toggle * more descriptions * more descriptions * update desc * param turn signals * sort * fix * always show desc if not available * should be bool * rocket fuel * steering arc * lint --------- Co-authored-by: Jason Wen Co-authored-by: DevTekVE --- common/params_keys.h | 2 +- .../ui/sunnypilot/layouts/settings/visuals.py | 126 +++++++++++++++++- .../onroad/blind_spot_indicators.py | 3 + .../ui/sunnypilot/onroad/circular_alerts.py | 10 +- .../ui/sunnypilot/onroad/hud_renderer.py | 4 +- selfdrive/ui/sunnypilot/onroad/road_name.py | 2 +- selfdrive/ui/sunnypilot/onroad/rocket_fuel.py | 5 + selfdrive/ui/sunnypilot/onroad/turn_signal.py | 3 + selfdrive/ui/sunnypilot/ui_state.py | 23 ++-- sunnypilot/sunnylink/params_metadata.json | 40 +++--- 10 files changed, 175 insertions(+), 43 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index 44584c9113..f2a63ec1b1 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -249,7 +249,7 @@ inline static std::unordered_map keys = { {"OsmStateTitle", {PERSISTENT, STRING}}, {"OsmWayTest", {PERSISTENT, STRING}}, {"RoadName", {CLEAR_ON_ONROAD_TRANSITION, STRING}}, - {"RoadNameToggle", {PERSISTENT, STRING}}, + {"RoadNameToggle", {PERSISTENT | BACKUP, BOOL, "0"}}, // Speed Limit {"SpeedLimitMode", {PERSISTENT | BACKUP, INT, "1"}}, diff --git a/selfdrive/ui/sunnypilot/layouts/settings/visuals.py b/selfdrive/ui/sunnypilot/layouts/settings/visuals.py index 1036af3e5f..84be5a26ab 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/visuals.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/visuals.py @@ -5,9 +5,18 @@ 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. """ from openpilot.common.params import Params +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.multilang import tr, tr_noop +from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, multiple_button_item_sp from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets import Widget +CHEVRON_INFO_DESCRIPTION = { + "enabled": tr_noop("Display useful metrics below the chevron that tracks the lead car " + + "only applicable to cars with sunnypilot longitudinal control."), + "disabled": tr_noop("This feature requires sunnypilot longitudinal control to be available.") +} + class VisualsLayout(Widget): def __init__(self): @@ -18,13 +27,128 @@ class VisualsLayout(Widget): self._scroller = Scroller(items, line_separator=True, spacing=0) def _initialize_items(self): - items = [ + self._toggle_defs = { + "BlindSpot": ( + lambda: tr("Show Blind Spot Warnings"), + tr("Enabling this will display warnings when a vehicle is detected in your " + + "blind spot as long as your car has BSM supported."), + None, + ), + "TorqueBar": ( + lambda: tr("Steering Arc"), + tr("Display steering arc on the driving screen when lateral control is enabled."), + None, + ), + "RainbowMode": ( + lambda: tr("Enable Tesla Rainbow Mode"), + tr("A beautiful rainbow effect on the path the model wants to take. " + + "It does not affect driving in any way."), + None, + ), + "StandstillTimer": ( + lambda: tr("Enable Standstill Timer"), + tr("Show a timer on the HUD when the car is at a standstill."), + None, + ), + "RoadNameToggle": ( + lambda: tr("Display Road Name"), + tr("Displays the name of the road the car is traveling on." + + "
The OpenStreetMap database of the location must be downloaded from " + + "the OSM panel to fetch the road name."), + None, + ), + "GreenLightAlert": ( + lambda: tr("Green Traffic Light Alert (Beta)"), + tr("A chime and on-screen alert will play when the traffic light you are waiting for " + + "turns green and you have no vehicle in front of you." + + "
Note: This chime is only designed as a notification. " + + "It is the driver's responsibility to observe their environment and make decisions accordingly."), + None, + ), + "LeadDepartAlert": ( + lambda: tr("Lead Departure Alert (Beta)"), + tr("A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving." + + "
Note: This chime is only designed as a notification. " + + "It is the driver's responsibility to observe their environment and make decisions accordingly."), + None, + ), + "TrueVEgoUI": ( + lambda: tr("Speedometer: Always Display True Speed"), + tr("For applicable vehicles, always display the true vehicle current speed from wheel speed sensors."), + None, + ), + "HideVEgoUI": ( + lambda: tr("Speedometer: Hide from Onroad Screen"), + tr("When enabled, the speedometer on the onroad screen is not displayed."), + None, + ), + "ShowTurnSignals": ( + lambda: tr("Display Turn Signals"), + tr("When enabled, visual turn indicators are drawn on the HUD."), + None, + ), + "RocketFuel": ( + lambda: tr("Real-time Acceleration Bar"), + tr("Show an indicator on the left side of the screen to display real-time vehicle acceleration and deceleration. " + + "This displays what the car is currently doing, not what the planner is requesting."), + None, + ), + } + self._toggles = {} + for param, (title, desc, callback) in self._toggle_defs.items(): + toggle = toggle_item_sp( + title=title, + description=desc, + param=param, + initial_state=ui_state.params.get_bool(param), + callback=callback, + ) + self._toggles[param] = toggle + self._chevron_info = multiple_button_item_sp( + title=lambda: tr("Display Metrics Below Chevron"), + description="", + buttons=[lambda: tr("Off"), lambda: tr("Distance"), lambda: tr("Speed"), lambda: tr("Time"), lambda: tr("All")], + param="ChevronInfo", + inline=False + ) + self._dev_ui_info = multiple_button_item_sp( + title=lambda: tr("Developer UI"), + description=lambda: tr("Display real-time parameters and metrics from various sources."), + buttons=[lambda: tr("Off"), lambda: tr("Bottom"), lambda: tr("Right"), lambda: tr("Right & Bottom")], + param="DevUIInfo", + button_width=350, + inline=False + ) + + items = list(self._toggles.values()) + [ + self._chevron_info, + self._dev_ui_info, ] return items + def _update_state(self): + super()._update_state() + + for param in self._toggle_defs: + self._toggles[param].action_item.set_state(self._params.get_bool(param)) + + self._dev_ui_info.action_item.set_selected_button(ui_state.params.get("DevUIInfo", return_default=True)) + + if ui_state.has_longitudinal_control: + self._chevron_info.set_description(tr(CHEVRON_INFO_DESCRIPTION["enabled"])) + self._chevron_info.action_item.set_selected_button(ui_state.params.get("ChevronInfo", return_default=True)) + self._chevron_info.action_item.set_enabled(True) + else: + self._chevron_info.set_description(tr(CHEVRON_INFO_DESCRIPTION["disabled"])) + self._chevron_info.action_item.set_enabled(False) + ui_state.params.put("ChevronInfo", 0) + def _render(self, rect): self._scroller.render(rect) def show_event(self): self._scroller.show_event() + if not ui_state.has_longitudinal_control: + self._chevron_info.set_description(tr(CHEVRON_INFO_DESCRIPTION["disabled"])) + self._chevron_info.show_description(True) diff --git a/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py b/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py index 1087579fef..61aa52537b 100644 --- a/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py +++ b/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py @@ -31,6 +31,9 @@ class BlindSpotIndicators: return self._blind_spot_left_alpha_filter.x > 0.01 or self._blind_spot_right_alpha_filter.x > 0.01 def render(self, rect: rl.Rectangle) -> None: + if not ui_state.blindspot: + return + BLIND_SPOT_MARGIN_X = 20 # Distance from edge of screen BLIND_SPOT_Y_OFFSET = 100 # Distance from top of screen diff --git a/selfdrive/ui/sunnypilot/onroad/circular_alerts.py b/selfdrive/ui/sunnypilot/onroad/circular_alerts.py index 965ce7fe77..8aa4c71d0d 100644 --- a/selfdrive/ui/sunnypilot/onroad/circular_alerts.py +++ b/selfdrive/ui/sunnypilot/onroad/circular_alerts.py @@ -23,7 +23,6 @@ class CircularAlertsRenderer: self._e2e_alert_frame = 0 self._green_light_alert = False self._lead_depart_alert = False - self._standstill_timer = False self._standstill_elapsed_time = 0.0 self._is_standstill = False self._alert_text = "" @@ -36,7 +35,6 @@ class CircularAlertsRenderer: car_state = sm['carState'] self._green_light_alert = lp_sp.e2eAlerts.greenLightAlert self._lead_depart_alert = lp_sp.e2eAlerts.leadDepartAlert - self._standstill_timer = ui_state.standstill_timer self._is_standstill = car_state.standstill if not ui_state.started: @@ -61,7 +59,7 @@ class CircularAlertsRenderer: self._alert_text = "LEAD VEHICLE\nDEPARTING" self._alert_img = self._lead_depart_alert_img - elif self._standstill_timer and self._is_standstill: + elif ui_state.standstill_timer and self._is_standstill: self._alert_img = None self._standstill_elapsed_time += 1.0 / gui_app.target_fps minute = int(self._standstill_elapsed_time / 60) @@ -75,7 +73,7 @@ class CircularAlertsRenderer: self._standstill_elapsed_time = 0.0 def render(self, rect: rl.Rectangle) -> None: - if not self._allow_e2e_alerts or (self._e2e_alert_display_timer <= 0 and not (self._standstill_timer and self._is_standstill)): + if not self._allow_e2e_alerts or (self._e2e_alert_display_timer <= 0 and not (ui_state.standstill_timer and self._is_standstill)): return e2e_alert_size = 250 @@ -91,7 +89,7 @@ class CircularAlertsRenderer: is_pulsing = (self._e2e_alert_frame % gui_app.target_fps) < (gui_app.target_fps / 2.5) # Standstill Timer (STOPPED) should be static white - if self._e2e_alert_display_timer == 0 and self._standstill_timer and self._is_standstill: + if self._e2e_alert_display_timer == 0 and ui_state.standstill_timer and self._is_standstill: frame_color = rl.Color(255, 255, 255, 75) else: frame_color = rl.Color(255, 255, 255, 75) if is_pulsing else rl.Color(0, 255, 0, 75) @@ -121,7 +119,7 @@ class CircularAlertsRenderer: # Draw lines upwards from bottom current_y = bottom_y - (len(lines) * text_size * FONT_SCALE) - if self._e2e_alert_display_timer == 0 and self._standstill_timer and self._is_standstill: + if self._e2e_alert_display_timer == 0 and ui_state.standstill_timer and self._is_standstill: # Standstill Timer Text alert_alt_text = "STOPPED" top_text_size = 80 diff --git a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py index 3b810d62e9..d8ba4b8bf0 100644 --- a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py +++ b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py @@ -59,6 +59,4 @@ class HudRendererSP(HudRenderer): self.smart_cruise_control_renderer.render(rect) self.turn_signal_controller.render(rect) self.circular_alerts_renderer.render(rect) - - if ui_state.rocket_fuel: - self.rocket_fuel.render(rect, ui_state.sm) + self.rocket_fuel.render(rect, ui_state.sm) diff --git a/selfdrive/ui/sunnypilot/onroad/road_name.py b/selfdrive/ui/sunnypilot/onroad/road_name.py index 652e620ad6..f85285ef53 100644 --- a/selfdrive/ui/sunnypilot/onroad/road_name.py +++ b/selfdrive/ui/sunnypilot/onroad/road_name.py @@ -31,7 +31,7 @@ class RoadNameRenderer(Widget): self.road_name = lmd.roadName def _render(self, rect: rl.Rectangle): - if not self.road_name: + if not self.road_name or not ui_state.road_name_toggle: return text = self.road_name diff --git a/selfdrive/ui/sunnypilot/onroad/rocket_fuel.py b/selfdrive/ui/sunnypilot/onroad/rocket_fuel.py index af25711a92..cb1012890e 100644 --- a/selfdrive/ui/sunnypilot/onroad/rocket_fuel.py +++ b/selfdrive/ui/sunnypilot/onroad/rocket_fuel.py @@ -6,12 +6,17 @@ See the LICENSE.md file in the root directory for more details. """ import pyray as rl +from openpilot.selfdrive.ui.ui_state import ui_state + class RocketFuel: def __init__(self): self.vc_accel = 0.0 def render(self, rect: rl.Rectangle, sm) -> None: + if not ui_state.rocket_fuel: + return + vc_accel0 = sm['carState'].aEgo # Smooth the acceleration diff --git a/selfdrive/ui/sunnypilot/onroad/turn_signal.py b/selfdrive/ui/sunnypilot/onroad/turn_signal.py index 3a66ffeb03..04fff7db76 100644 --- a/selfdrive/ui/sunnypilot/onroad/turn_signal.py +++ b/selfdrive/ui/sunnypilot/onroad/turn_signal.py @@ -137,6 +137,9 @@ class TurnSignalController: self._right_signal.deactivate() def render(self, rect: rl.Rectangle): + if not ui_state.turn_signals: + return + x = rect.x + rect.width / 2 left_x = x - self._config.left_x - self._config.size diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 6d7eab51cd..6403157d5c 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -120,22 +120,23 @@ class UIStateSP: CP_SP_bytes = self.params.get("CarParamsSPPersistent") if CP_SP_bytes is not None: self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP) - self.sunnylink_enabled = self.params.get_bool("SunnylinkEnabled") - self.developer_ui = self.params.get("DevUIInfo") - self.rocket_fuel = self.params.get_bool("RocketFuel") - self.rainbow_path = self.params.get_bool("RainbowMode") - self.chevron_metrics = self.params.get("ChevronInfo") - self.torque_bar = self.params.get_bool("TorqueBar") self.active_bundle = self.params.get("ModelManager_ActiveBundle") + self.blindspot = self.params.get_bool("BlindSpot") + self.chevron_metrics = self.params.get("ChevronInfo") self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True) - self.speed_limit_mode = self.params.get("SpeedLimitMode", return_default=True) - self.standstill_timer = self.params.get_bool("StandstillTimer") - self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI") + self.developer_ui = self.params.get("DevUIInfo") self.hide_v_ego_ui = self.params.get_bool("HideVEgoUI") - - # Onroad Screen Brightness self.onroad_brightness = int(float(self.params.get("OnroadScreenOffBrightness", return_default=True))) self.onroad_brightness_timer_param = self.params.get("OnroadScreenOffTimer", return_default=True) + self.rainbow_path = self.params.get_bool("RainbowMode") + self.road_name_toggle = self.params.get_bool("RoadNameToggle") + self.rocket_fuel = self.params.get_bool("RocketFuel") + self.speed_limit_mode = self.params.get("SpeedLimitMode", return_default=True) + self.standstill_timer = self.params.get_bool("StandstillTimer") + self.sunnylink_enabled = self.params.get_bool("SunnylinkEnabled") + self.torque_bar = self.params.get_bool("TorqueBar") + self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI") + self.turn_signals = self.params.get_bool("ShowTurnSignals") class DeviceSP: diff --git a/sunnypilot/sunnylink/params_metadata.json b/sunnypilot/sunnylink/params_metadata.json index 090beb49f0..c0c84eac09 100644 --- a/sunnypilot/sunnylink/params_metadata.json +++ b/sunnypilot/sunnylink/params_metadata.json @@ -90,8 +90,8 @@ "description": "" }, "BlindSpot": { - "title": "Blind Spot Detection", - "description": "" + "title": "[TIZI/TICI only] Blind Spot Detection", + "description": "Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported." }, "BlinkerMinLateralControlSpeed": { "title": "Blinker Min Lateral Control Speed", @@ -339,8 +339,8 @@ "description": "" }, "GreenLightAlert": { - "title": "Green Light Alert", - "description": "" + "title": "Green Traffic Light Alert (Beta)", + "description": "A chime and on-screen alert (TIZI/TICI only) will play when the traffic light you are waiting for turns green and you have no vehicle in front of you.
Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly." }, "GsmApn": { "title": "GSM APN", @@ -367,8 +367,8 @@ "description": "" }, "HideVEgoUI": { - "title": "Hide vEgo UI", - "description": "" + "title": "[TIZI/TICI only] Speedometer: Hide from Onroad Screen", + "description": "When enabled, the speedometer on the onroad screen is not displayed." }, "HyundaiLongitudinalTuning": { "title": "Hyundai Longitudinal Tuning", @@ -585,8 +585,8 @@ "description": "" }, "LeadDepartAlert": { - "title": "Lead Depart Alert", - "description": "" + "title": "Lead Departure Alert (Beta)", + "description": "A chime and on-screen alert (TIZI/TICI only) will play when you are stopped, and the vehicle in front of you start moving.
Note: This chime is only designed as a notification. It is the driver's responsibility to observe their environment and make decisions accordingly." }, "LiveDelay": { "title": "Live Delay", @@ -1079,12 +1079,12 @@ "description": "" }, "RoadNameToggle": { - "title": "Display Road Name", - "description": "" + "title": "[TIZI/TICI only] Display Road Name", + "description": "Displays the name of the road the car is traveling on.
The OpenStreetMap database of the location must be downloaded to fetch the road name." }, "RocketFuel": { - "title": "Display Rocket Fuel Bar", - "description": "Show an indicator on the left side of the screen to display real-time vehicle acceleration and deceleration." + "title": "[TIZI/TICI only] Real-time Acceleration Bar", + "description": "Show an indicator on the left side of the screen to display real-time vehicle acceleration and deceleration. This displays what the car is currently doing, not what the planner is requesting." }, "RouteCount": { "title": "Route Count", @@ -1103,8 +1103,8 @@ "description": "" }, "ShowTurnSignals": { - "title": "Show Turn Signals", - "description": "" + "title": "[TIZI/TICI only] Display Turn Signals", + "description": "When enabled, visual turn indicators are drawn on the HUD." }, "SmartCruiseControlMap": { "title": "Smart Cruise Control - Map", @@ -1196,8 +1196,8 @@ "description": "" }, "StandstillTimer": { - "title": "Standstill Timer", - "description": "" + "title": "[TIZI/TICI only] Standstill Timer", + "description": "Show a timer on the HUD when the car is at a standstill." }, "SubaruStopAndGo": { "title": "Subaru Stop and Go", @@ -1240,8 +1240,8 @@ "description": "" }, "TorqueBar": { - "title": "Steering Arc", - "description": "[TIZI/TICI only] Display steering arc on the driving screen when lateral control is enabled." + "title": "[TIZI/TICI only] Steering Arc", + "description": "Display steering arc on the driving screen when lateral control is enabled." }, "TorqueParamsOverrideEnabled": { "title": "Manual Real-Time Tuning", @@ -1271,8 +1271,8 @@ "description": "" }, "TrueVEgoUI": { - "title": "True vEgo UI", - "description": "" + "title": "[TIZI/TICI only] Speedometer: Always Display True Speed", + "description": "For applicable vehicles, always display the true vehicle current speed from wheel speed sensors." }, "UbloxAvailable": { "title": "Ublox Available", From 1f778c8c23df8508fff09c3535f05356c4fa683d Mon Sep 17 00:00:00 2001 From: Nayan Date: Mon, 9 Feb 2026 00:38:39 -0500 Subject: [PATCH 11/11] Device: Retain QuickBoot state after op switch (#1333) Device: Retain QuickBoot state after SSH Update Co-authored-by: Jason Wen Co-authored-by: DevTekVE --- system/manager/manager.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/system/manager/manager.py b/system/manager/manager.py index 36e45488f6..8c5d35d072 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -20,6 +20,7 @@ from openpilot.system.athena.registration import register, UNREGISTERED_DONGLE_I from openpilot.common.swaglog import cloudlog, add_file_handler from openpilot.system.version import get_build_metadata from openpilot.system.hardware.hw import Paths +from openpilot.system.hardware import PC def manager_init() -> None: @@ -39,6 +40,12 @@ def manager_init() -> None: if params.get("DeviceBootMode") == 1: # start in Always Offroad mode params.put_bool("OffroadMode", True) + # quick boot + if params.get_bool("QuickBootToggle") and not PC: + prebuilt_path = "/data/openpilot/prebuilt" + if not os.path.exists(prebuilt_path): + open(prebuilt_path, 'x').close() + if params.get_bool("RecordFrontLock"): params.put_bool("RecordFront", True)