From a30fc9bcd2d044e3f11b102fc443da5d15a7d314 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Thu, 1 Jan 2026 12:23:01 -0700 Subject: [PATCH 1/6] modeld: configurable camera offset (#1614) * modeld: configurable camera offset Negative Values: Shears the image to the left, moving the models center to the Right. Positive Value: Shears the image to the right, moving the models center to the Left. * modeld: camera offset class * verify zero offset I @ A = A * slithered and slunked * Update params_metadata.json * wait * Update model_renderer.py * Update model_renderer.py * requested changes * stricter * Update model_renderer.py * more * return default * Update params_metadata.json * final --------- Co-authored-by: Jason Wen --- common/params_keys.h | 1 + selfdrive/ui/mici/onroad/model_renderer.py | 15 +++- selfdrive/ui/onroad/model_renderer.py | 15 ++-- selfdrive/ui/sunnypilot/ui_state.py | 1 + sunnypilot/modeld/modeld.py | 4 + sunnypilot/modeld_v2/camera_offset_helper.py | 39 +++++++++ sunnypilot/modeld_v2/modeld.py | 7 +- .../tests/test_camera_offset_helper.py | 84 +++++++++++++++++++ sunnypilot/sunnylink/params_metadata.json | 7 ++ 9 files changed, 162 insertions(+), 11 deletions(-) create mode 100644 sunnypilot/modeld_v2/camera_offset_helper.py create mode 100644 sunnypilot/modeld_v2/tests/test_camera_offset_helper.py diff --git a/common/params_keys.h b/common/params_keys.h index 054ce97e6d..a559d411e4 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -221,6 +221,7 @@ inline static std::unordered_map keys = { {"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}}, // sunnypilot model params + {"CameraOffset", {PERSISTENT | BACKUP, FLOAT, "0.0"}}, {"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}}, {"LagdToggleDelay", {PERSISTENT | BACKUP, FLOAT, "0.2"}}, {"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}}, diff --git a/selfdrive/ui/mici/onroad/model_renderer.py b/selfdrive/ui/mici/onroad/model_renderer.py index db316aa636..0908de0bf4 100644 --- a/selfdrive/ui/mici/onroad/model_renderer.py +++ b/selfdrive/ui/mici/onroad/model_renderer.py @@ -80,6 +80,9 @@ class ModelRenderer(Widget): self._transform_dirty = True self._clip_region = None + self._counter = -1 + self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0 + self._exp_gradient = Gradient( start=(0.0, 1.0), # Bottom of path end=(0.0, 0.0), # Top of path @@ -99,6 +102,10 @@ class ModelRenderer(Widget): def _render(self, rect: rl.Rectangle): sm = ui_state.sm + if self._counter % 180 == 0: # This runs at 60fps, so we query every 3 seconds + self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0 + self._counter += 1 + self._torque_filter.update(-ui_state.sm['carOutput'].actuatorsOutput.torque) # Check if data is up-to-date @@ -150,13 +157,13 @@ class ModelRenderer(Widget): def _update_raw_points(self, model): """Update raw 3D points from model data""" - self._path.raw_points = np.array([model.position.x, model.position.y, model.position.z], dtype=np.float32).T + self._path.raw_points = np.array([model.position.x, np.array(model.position.y) + self._camera_offset, model.position.z], dtype=np.float32).T for i, lane_line in enumerate(model.laneLines): - self._lane_lines[i].raw_points = np.array([lane_line.x, lane_line.y, lane_line.z], dtype=np.float32).T + self._lane_lines[i].raw_points = np.array([lane_line.x, np.array(lane_line.y) + self._camera_offset, lane_line.z], dtype=np.float32).T for i, road_edge in enumerate(model.roadEdges): - self._road_edges[i].raw_points = np.array([road_edge.x, road_edge.y, road_edge.z], dtype=np.float32).T + self._road_edges[i].raw_points = np.array([road_edge.x, np.array(road_edge.y) + self._camera_offset, road_edge.z], dtype=np.float32).T self._lane_line_probs = np.array(model.laneLineProbs, dtype=np.float32) self._road_edge_stds = np.array(model.roadEdgeStds, dtype=np.float32) @@ -174,7 +181,7 @@ class ModelRenderer(Widget): # Get z-coordinate from path at the lead vehicle position z = self._path.raw_points[idx, 2] if idx < len(self._path.raw_points) else 0.0 - point = self._map_to_screen(d_rel, -y_rel, z + self._path_offset_z) + point = self._map_to_screen(d_rel, -y_rel + self._camera_offset, z + self._path_offset_z) if point: self._lead_vehicles[i] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect) diff --git a/selfdrive/ui/onroad/model_renderer.py b/selfdrive/ui/onroad/model_renderer.py index cae9765341..353cc5aa40 100644 --- a/selfdrive/ui/onroad/model_renderer.py +++ b/selfdrive/ui/onroad/model_renderer.py @@ -56,7 +56,8 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP): self._road_edge_stds = np.zeros(2, dtype=np.float32) self._lead_vehicles = [LeadVehicle(), LeadVehicle()] self._path_offset_z = HEIGHT_INIT[0] - + self._counter = -1 + self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0 # Initialize ModelPoints objects self._path = ModelPoints() self._lane_lines = [ModelPoints() for _ in range(4)] @@ -103,6 +104,10 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP): live_calib = sm['liveCalibration'] self._path_offset_z = live_calib.height[0] if live_calib.height else HEIGHT_INIT[0] + if self._counter % 60 == 0: + self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0 + self._counter += 1 + if sm.updated['carParams']: self._longitudinal_control = sm['carParams'].openpilotLongitudinalControl @@ -136,13 +141,13 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP): def _update_raw_points(self, model): """Update raw 3D points from model data""" - self._path.raw_points = np.array([model.position.x, model.position.y, model.position.z], dtype=np.float32).T + self._path.raw_points = np.array([model.position.x, np.array(model.position.y) + self._camera_offset, model.position.z], dtype=np.float32).T for i, lane_line in enumerate(model.laneLines): - self._lane_lines[i].raw_points = np.array([lane_line.x, lane_line.y, lane_line.z], dtype=np.float32).T + self._lane_lines[i].raw_points = np.array([lane_line.x, np.array(lane_line.y) + self._camera_offset, lane_line.z], dtype=np.float32).T for i, road_edge in enumerate(model.roadEdges): - self._road_edges[i].raw_points = np.array([road_edge.x, road_edge.y, road_edge.z], dtype=np.float32).T + self._road_edges[i].raw_points = np.array([road_edge.x, np.array(road_edge.y) + self._camera_offset, road_edge.z], dtype=np.float32).T self._lane_line_probs = np.array(model.laneLineProbs, dtype=np.float32) self._road_edge_stds = np.array(model.roadEdgeStds, dtype=np.float32) @@ -160,7 +165,7 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP): # Get z-coordinate from path at the lead vehicle position z = self._path.raw_points[idx, 2] if idx < len(self._path.raw_points) else 0.0 - point = self._map_to_screen(d_rel, -y_rel, z + self._path_offset_z) + point = self._map_to_screen(d_rel, -y_rel + self._camera_offset, z + self._path_offset_z) if point: self._lead_vehicles[i] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect) diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index ca8125512a..21ed78b096 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -73,6 +73,7 @@ class UIStateSP: self.developer_ui = self.params.get("DevUIInfo") self.rainbow_path = self.params.get_bool("RainbowMode") self.chevron_metrics = self.params.get("ChevronInfo") + self.active_bundle = self.params.get("ModelManager_ActiveBundle") class DeviceSP: diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py index 3d11ed23f4..b36aea405a 100755 --- a/sunnypilot/modeld/modeld.py +++ b/sunnypilot/modeld/modeld.py @@ -24,6 +24,7 @@ from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.modeld.runners import ModelRunner, Runtime from openpilot.sunnypilot.modeld.parse_model_outputs import Parser from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState +from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan from openpilot.sunnypilot.models.helpers import get_active_bundle, get_model_path, load_metadata, prepare_inputs, load_meta_constants from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext @@ -195,6 +196,7 @@ def main(demo=False): buf_main, buf_extra = None, None meta_main = FrameMeta() meta_extra = FrameMeta() + camera_offset_helper = CameraOffsetHelper() if demo: @@ -250,12 +252,14 @@ def main(demo=False): frame_id = sm["roadCameraState"].frameId if sm.frame % 60 == 0: model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay) + camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True)) lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] model_transform_main = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics, False).astype(np.float32) model_transform_extra = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics, True).astype(np.float32) + model_transform_main, model_transform_extra = camera_offset_helper.update(model_transform_main, model_transform_extra, sm, main_wide_camera) live_calib_seen = True traffic_convention = np.zeros(2) diff --git a/sunnypilot/modeld_v2/camera_offset_helper.py b/sunnypilot/modeld_v2/camera_offset_helper.py new file mode 100644 index 0000000000..7502c3eeb0 --- /dev/null +++ b/sunnypilot/modeld_v2/camera_offset_helper.py @@ -0,0 +1,39 @@ +""" +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 numpy as np + +from openpilot.common.transformations.camera import DEVICE_CAMERAS + + +class CameraOffsetHelper: + def __init__(self): + self.camera_offset = 0.0 + self.actual_camera_offset = 0.0 + + @staticmethod + def apply_camera_offset(model_transform, intrinsics, height, offset_param): + cy = intrinsics[1, 2] + shear = np.eye(3, dtype=np.float32) + shear[0, 1] = offset_param / height + shear[0, 2] = -offset_param / height * cy + model_transform = (shear @ model_transform).astype(np.float32) + return model_transform + + def set_offset(self, offset): + self.camera_offset = offset + + def update(self, model_transform_main, model_transform_extra, sm, main_wide_camera): + self.actual_camera_offset = (0.9 * self.actual_camera_offset) + (0.1 * self.camera_offset) + dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] + height = sm["liveCalibration"].height[0] if sm['liveCalibration'].height else 1.22 + + intrinsics_main = dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics + model_transform_main = self.apply_camera_offset(model_transform_main, intrinsics_main, height, self.actual_camera_offset) + + intrinsics_extra = dc.ecam.intrinsics + model_transform_extra = self.apply_camera_offset(model_transform_extra, intrinsics_extra, height, self.actual_camera_offset) + return model_transform_main, model_transform_extra diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index b3b1d35fd9..be0724db0d 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -21,6 +21,7 @@ from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_p from openpilot.sunnypilot.modeld_v2.constants import Plan from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLContext from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants +from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase @@ -230,6 +231,7 @@ def main(demo=False): buf_main, buf_extra = None, None meta_main = FrameMeta() meta_extra = FrameMeta() + camera_offset_helper = CameraOffsetHelper() if demo: @@ -285,13 +287,14 @@ def main(demo=False): if sm.frame % 60 == 0: model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay) model.PLANPLUS_CONTROL = params.get("PlanplusControl", return_default=True) + camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True)) lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] - model_transform_main = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics, - False).astype(np.float32) + model_transform_main = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics, False).astype(np.float32) model_transform_extra = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics, True).astype(np.float32) + model_transform_main, model_transform_extra = camera_offset_helper.update(model_transform_main, model_transform_extra, sm, main_wide_camera) live_calib_seen = True traffic_convention = np.zeros(2) diff --git a/sunnypilot/modeld_v2/tests/test_camera_offset_helper.py b/sunnypilot/modeld_v2/tests/test_camera_offset_helper.py new file mode 100644 index 0000000000..f25bcd0a35 --- /dev/null +++ b/sunnypilot/modeld_v2/tests/test_camera_offset_helper.py @@ -0,0 +1,84 @@ +""" +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 numpy as np + +from openpilot.common.transformations.camera import DEVICE_CAMERAS +from openpilot.common.transformations.model import get_warp_matrix +from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper + + +class MockStruct: + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + def __getitem__(self, item): + return getattr(self, item) + + +class TestCameraOffset: + def setup_method(self): + self.camera_offset = CameraOffsetHelper() + self.dc = DEVICE_CAMERAS[('mici', 'os04c10')] + + def test_smoothing(self): + self.camera_offset.set_offset(0.2) + + sm = MockStruct( + deviceState=MockStruct(deviceType='mici'), + roadCameraState=MockStruct(sensor='os04c10'), + liveCalibration=MockStruct(rpyCalib=[0.0, 0.0, 0.0], height=[1.22]) + ) + + intrinsics_main = self.dc.fcam.intrinsics + intrinsics_extra = self.dc.ecam.intrinsics + device_from_calib_euler = np.array([0.0, 0.0, 0.0], dtype=np.float32) + main_transform = get_warp_matrix(device_from_calib_euler, intrinsics_main, False).astype(np.float32) + extra_transform = get_warp_matrix(device_from_calib_euler, intrinsics_extra, True).astype(np.float32) + + self.camera_offset.update(main_transform, extra_transform, sm, False) + np.testing.assert_almost_equal(self.camera_offset.actual_camera_offset, 0.02) + self.camera_offset.update(main_transform, extra_transform, sm, False) + np.testing.assert_almost_equal(self.camera_offset.actual_camera_offset, 0.038) + + def test_camera_offset_(self): + intrinsics = self.dc.fcam.intrinsics + transform = np.eye(3, dtype=np.float32) + height = 1.22 + offset = 0.1 + + cy = intrinsics[1, 2] + expected_shear = np.eye(3, dtype=np.float32) + expected_shear[0, 1] = offset / height + expected_shear[0, 2] = -offset / height * cy + + result = CameraOffsetHelper.apply_camera_offset(transform, intrinsics, height, offset) + np.testing.assert_array_almost_equal(result, expected_shear) + + def test_update(self): + sm = MockStruct( + deviceState=MockStruct(deviceType='mici'), + roadCameraState=MockStruct(sensor='os04c10'), + liveCalibration=MockStruct(rpyCalib=[0.0, 0.0, 0.0], height=[1.22]) + ) + intrinsics_main = self.dc.fcam.intrinsics + intrinsics_extra = self.dc.ecam.intrinsics + device_from_calib_euler = np.array([0.0, 0.0, 0.0], dtype=np.float32) + main_transform = get_warp_matrix(device_from_calib_euler, intrinsics_main, False).astype(np.float32) + extra_transform = get_warp_matrix(device_from_calib_euler, intrinsics_extra, True).astype(np.float32) + + self.camera_offset.set_offset(0.0) # test default offset doesn't change transformation + main_out, extra_out = self.camera_offset.update(main_transform, extra_transform, sm, False) + np.testing.assert_array_equal(main_out, main_transform) + np.testing.assert_array_equal(extra_out, extra_transform) + + self.camera_offset.set_offset(0.2) # test valid offset changes transformation + main_out, extra_out = self.camera_offset.update(main_transform, extra_transform, sm, False) + assert not np.array_equal(main_out, main_transform) + assert not np.array_equal(extra_out, extra_transform) + assert main_out[0, 1] != 0.0 + assert main_out[0, 2] != 0.0 diff --git a/sunnypilot/sunnylink/params_metadata.json b/sunnypilot/sunnylink/params_metadata.json index 95a143ea66..cccf98d796 100644 --- a/sunnypilot/sunnylink/params_metadata.json +++ b/sunnypilot/sunnylink/params_metadata.json @@ -121,6 +121,13 @@ "title": "Camera Debug Exp Time", "description": "" }, + "CameraOffset": { + "title": "Adjust Camera Offset on non Default Model", + "description": "Adjust this to center the car in its lane by virtually shifting the camera's perspective. Negative Values: Shears the image to the left, moving the model's center to the Right; Positive Values: Shears the image to the right, moving the model's center to the Left. Note: these values are in meters.", + "min": -0.35, + "max": 0.35, + "step": 0.01 + }, "CarBatteryCapacity": { "title": "Car Battery Capacity", "description": "" From 50b8ae9e09b2e5a428ff2b45d8c062a700602d26 Mon Sep 17 00:00:00 2001 From: Nayan Date: Sat, 3 Jan 2026 08:35:02 -0500 Subject: [PATCH 2/6] sunnylink: update params metadata (#1636) * sunnylink model controls * cleanup more controls * update verbiage Co-authored-by: DevTekVE --------- Co-authored-by: DevTekVE --- sunnypilot/sunnylink/params_metadata.json | 67 ++++++++++++----------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/sunnypilot/sunnylink/params_metadata.json b/sunnypilot/sunnylink/params_metadata.json index cccf98d796..f0ddbc9548 100644 --- a/sunnypilot/sunnylink/params_metadata.json +++ b/sunnypilot/sunnylink/params_metadata.json @@ -122,8 +122,8 @@ "description": "" }, "CameraOffset": { - "title": "Adjust Camera Offset on non Default Model", - "description": "Adjust this to center the car in its lane by virtually shifting the camera's perspective. Negative Values: Shears the image to the left, moving the model's center to the Right; Positive Values: Shears the image to the right, moving the model's center to the Left. Note: these values are in meters.", + "title": "Adjust Camera Offset", + "description": "Virtually shift camera's perspective to move model's center to Left(+ values) or Right (- values)", "min": -0.35, "max": 0.35, "step": 0.01 @@ -193,7 +193,7 @@ "description": "" }, "CustomAccIncrementsEnabled": { - "title": "Custom ACC Increments Enabled", + "title": "Custom ACC Increments", "description": "" }, "CustomAccLongPressIncrement": { @@ -211,8 +211,8 @@ "step": 1 }, "CustomTorqueParams": { - "title": "Custom Torque Params", - "description": "" + "title": "Enable Custom Torque Tuning", + "description": "Enables custom tuning for Torque lateral control" }, "DevUIInfo": { "title": "Developer UI Info", @@ -286,7 +286,7 @@ }, "EnforceTorqueControl": { "title": "Enforce Torque Control", - "description": "" + "description": "Enable this to enforce sunnypilot to steer with Torque lateral control." }, "ExperimentalMode": { "title": "Experimental Mode", @@ -451,12 +451,15 @@ "description": "" }, "LagdToggle": { - "title": "LaGD Toggle", - "description": "" + "title": "Live Learning Steer Delay", + "description": "Allow device to learn and adapt car's steering response time" }, "LagdToggleDelay": { - "title": "LaGD Toggle Delay", - "description": "" + "title": "Manual Software Delay", + "description": "Software delay to use when Live Learning Steer Delay is toggled off", + "min": 0.05, + "max": 0.5, + "step": 0.01 }, "LagdValueCache": { "title": "LaGD Value Cache", @@ -464,11 +467,11 @@ }, "LaneTurnDesire": { "title": "Lane Turn Desire", - "description": "" + "description": "Force model to plan an intent to turn based on blinker" }, "LaneTurnValue": { - "title": "Lane Turn Value", - "description": "", + "title": "Lane Turn Speed", + "description": "Maximum speed for lane turn desire", "min": 0, "max": 20, "step": 1 @@ -546,12 +549,12 @@ "description": "" }, "LiveTorqueParamsRelaxedToggle": { - "title": "Live Torque Params Relaxed Toggle", - "description": "" + "title": "Less Restrict Settings for Self-Tune (Beta)", + "description": "Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values." }, "LiveTorqueParamsToggle": { - "title": "Live Torque Params Toggle", - "description": "" + "title": "Self-Tune", + "description": "Enables self-tune for Torque lateral control" }, "LocationFilterInitialState": { "title": "Location Filter Initial State", @@ -704,7 +707,7 @@ "description": "" }, "OffroadMode": { - "title": "Offroad Mode", + "title": "Force Offroad Mode", "description": "" }, "Offroad_CarUnrecognized": { @@ -768,18 +771,18 @@ "description": "" }, "OnroadScreenOffBrightness": { - "title": "Onroad Screen Off Brightness", + "title": "Onroad Brightness", "description": "", "min": 0, "max": 100, "step": 5 }, "OnroadScreenOffControl": { - "title": "Onroad Screen Off Control", - "description": "" + "title": "Onroad Screen: Reduced Brightness", + "description": "Turn off device screen or reduce brightness after driving starts" }, "OnroadScreenOffTimer": { - "title": "Onroad Screen Off Timer", + "title": "Onroad Brightness Delay", "description": "", "min": 0, "max": 60, @@ -885,7 +888,7 @@ "description": "" }, "RoadNameToggle": { - "title": "Road Name Toggle", + "title": "Display Road Name", "description": "" }, "RouteCount": { @@ -898,7 +901,7 @@ }, "ShowAdvancedControls": { "title": "Show Advanced Controls", - "description": "" + "description": "Enable to show advanced controls on device" }, "ShowDebugInfo": { "title": "UI Debug Mode", @@ -909,11 +912,11 @@ "description": "" }, "SmartCruiseControlMap": { - "title": "Smart Cruise Control Map", + "title": "Smart Cruise Control - Map", "description": "" }, "SmartCruiseControlVision": { - "title": "Smart Cruise Control Vision", + "title": "Smart Cruise Control - Vision", "description": "" }, "SnoozeUpdate": { @@ -921,7 +924,7 @@ "description": "" }, "SpeedLimitMode": { - "title": "Speed Limit Mode", + "title": "Speed Limit Assist Mode", "description": "", "options": [ { @@ -961,7 +964,7 @@ ] }, "SpeedLimitPolicy": { - "title": "Speed Limit Policy", + "title": "Speed Limit Source", "description": "", "options": [ { @@ -987,7 +990,7 @@ ] }, "SpeedLimitValueOffset": { - "title": "Speed Limit Value Offset", + "title": "Speed Limit Offset Value", "description": "", "min": -30, "max": 30, @@ -1042,18 +1045,18 @@ "description": "" }, "TorqueParamsOverrideEnabled": { - "title": "Torque Params Override Enabled", + "title": "Manual Real-Time Tuning", "description": "" }, "TorqueParamsOverrideFriction": { - "title": "Torque Params Override Friction", + "title": "Manual Tune - Friction", "description": "", "min": 0.0, "max": 1.0, "step": 0.01 }, "TorqueParamsOverrideLatAccelFactor": { - "title": "Torque Params Override Lat Accel Factor", + "title": "Manual Tune - Lateral Acceleration Factor", "description": "", "min": 0.1, "max": 5.0, From 9a04a5eaae0e5fea4cb7b5221d11aa68e2d0587d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 3 Jan 2026 15:54:55 -0500 Subject: [PATCH 3/6] [bot] Update Python packages (#1565) * Update Python packages * no --------- Co-authored-by: github-actions[bot] Co-authored-by: Jason Wen --- opendbc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc_repo b/opendbc_repo index 74ac678501..e03fbf9be8 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 74ac6785011b2861b822651f51d0cd2f01ce79d2 +Subproject commit e03fbf9be8d063ad8aee260a67338e1dadea8037 From 987f53e69a145a7c02567bdcefc09674f5042494 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 21:01:21 -0500 Subject: [PATCH 4/6] [TIZI/TICI] ui: sunnylink status on sidebar (#1638) * Initial plan * feat: add sunnylink status metric Co-authored-by: devtekve <7696966+devtekve@users.noreply.github.com> * chore: extract sidebar constants Co-authored-by: devtekve <7696966+devtekve@users.noreply.github.com> * refactor: guard metric spacing Co-authored-by: devtekve <7696966+devtekve@users.noreply.github.com> * chore: clarify sunnylink helpers Co-authored-by: devtekve <7696966+devtekve@users.noreply.github.com> * refactor: guard metric spacing edge cases Co-authored-by: devtekve <7696966+devtekve@users.noreply.github.com> * chore: simplify spacing guards Co-authored-by: devtekve <7696966+devtekve@users.noreply.github.com> * chore: normalize sunnylink params Co-authored-by: devtekve <7696966+devtekve@users.noreply.github.com> * chore: harden sunnylink param parsing Co-authored-by: devtekve <7696966+devtekve@users.noreply.github.com> * chore: add param decode helper Co-authored-by: devtekve <7696966+devtekve@users.noreply.github.com> * chore: simplify sidebar metric spacing Co-authored-by: devtekve <7696966+devtekve@users.noreply.github.com> * chore: update sunnylink status color logic for improved clarity * sunnylink: update status handling to reflect offline state and improve fault indication sunnylink: enhance status handling with temporary fault indication * sunnylink: enhance status update logic for improved accuracy and clarity * make it int * Ugly with zero value, but done. Now we only need to remember to check the new sidebar if the old sidebar ever changes * Revert "Ugly with zero value, but done. Now we only need to remember to check the new sidebar if the old sidebar ever changes" This reverts commit 2d3b740e382206997885d47c60585b929baa773f. * decouple * no bad bot --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devtekve <7696966+devtekve@users.noreply.github.com> Co-authored-by: DevTekVE Co-authored-by: Jason Wen --- selfdrive/ui/layouts/sidebar.py | 15 +++- selfdrive/ui/sunnypilot/layouts/sidebar.py | 87 ++++++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/layouts/sidebar.py diff --git a/selfdrive/ui/layouts/sidebar.py b/selfdrive/ui/layouts/sidebar.py index 050cd795bf..bfa60c88ed 100644 --- a/selfdrive/ui/layouts/sidebar.py +++ b/selfdrive/ui/layouts/sidebar.py @@ -9,6 +9,8 @@ from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget +from openpilot.selfdrive.ui.sunnypilot.layouts.sidebar import SidebarSP + SIDEBAR_WIDTH = 300 METRIC_HEIGHT = 126 METRIC_WIDTH = 240 @@ -62,9 +64,10 @@ class MetricData: self.color = color -class Sidebar(Widget): +class Sidebar(Widget, SidebarSP): def __init__(self): - super().__init__() + Widget.__init__(self) + SidebarSP.__init__(self) self._net_type = NETWORK_TYPES.get(NetworkType.none) self._net_strength = 0 @@ -112,6 +115,7 @@ class Sidebar(Widget): self._update_temperature_status(device_state) self._update_connection_status(device_state) self._update_panda_status() + SidebarSP._update_sunnylink_status(self) def _update_network_status(self, device_state): self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, tr_noop("Unknown")) @@ -200,6 +204,13 @@ class Sidebar(Widget): rl.draw_text_ex(self._font_regular, tr(self._net_type), text_pos, FONT_SIZE, 0, Colors.WHITE) def _draw_metrics(self, rect: rl.Rectangle): + if gui_app.sunnypilot_ui(): + metrics, start_y, spacing = SidebarSP._draw_metrics_w_sunnylink(self, rect, self._temp_status, self._panda_status, self._connect_status) + for idx, metric in enumerate(metrics): + self._draw_metric(rect, metric, start_y + idx * spacing) + + return + metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)] for metric, y_offset in metrics: diff --git a/selfdrive/ui/sunnypilot/layouts/sidebar.py b/selfdrive/ui/sunnypilot/layouts/sidebar.py new file mode 100644 index 0000000000..79bb15dbb8 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/sidebar.py @@ -0,0 +1,87 @@ +""" +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 +import time +from dataclasses import dataclass +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID +from openpilot.system.ui.lib.multilang import tr_noop + + +PING_TIMEOUT_NS = 80_000_000_000 # 80 seconds in nanoseconds +METRIC_HEIGHT = 126 +METRIC_MARGIN = 30 +METRIC_START_Y = 300 +HOME_BTN = rl.Rectangle(60, 860, 180, 180) + + +# Color scheme +class Colors: + WHITE = rl.WHITE + WHITE_DIM = rl.Color(255, 255, 255, 85) + GRAY = rl.Color(84, 84, 84, 255) + + # Status colors + GOOD = rl.WHITE + WARNING = rl.Color(218, 202, 37, 255) + DANGER = rl.Color(201, 34, 49, 255) + PROGRESS = rl.Color(0, 134, 233, 255) + DISABLED = rl.Color(128, 128, 128, 255) + + # UI elements + METRIC_BORDER = rl.Color(255, 255, 255, 85) + BUTTON_NORMAL = rl.WHITE + BUTTON_PRESSED = rl.Color(255, 255, 255, 166) + + +@dataclass(slots=True) +class MetricData: + label: str + value: str + color: rl.Color + + def update(self, label: str, value: str, color: rl.Color): + self.label = label + self.value = value + self.color = color + + +class SidebarSP: + def __init__(self): + self._sunnylink_status = MetricData(tr_noop("SUNNYLINK"), tr_noop("OFFLINE"), Colors.WARNING) + + def _update_sunnylink_status(self): + if not ui_state.params.get_bool("SunnylinkEnabled"): + self._sunnylink_status.update(tr_noop("SUNNYLINK"), tr_noop("DISABLED"), Colors.DISABLED) + return + + last_ping = ui_state.params.get("LastSunnylinkPingTime") or 0 + dongle_id = ui_state.params.get("SunnylinkDongleId") + + is_online = last_ping and (time.monotonic_ns() - last_ping) < PING_TIMEOUT_NS + is_temp_fault = ui_state.params.get_bool("SunnylinkTempFault") + is_registering = not is_temp_fault and dongle_id in (None, "", UNREGISTERED_SUNNYLINK_DONGLE_ID) + + # Determine status/color pair based on priority + if last_ping: + status, color = (tr_noop("ONLINE"), Colors.GOOD) if is_online else (tr_noop("ERROR"), Colors.DANGER) + elif is_temp_fault: + status, color = (tr_noop("FAULT"), Colors.WARNING) + elif is_registering: + status, color = (tr_noop("REGIST..."), Colors.PROGRESS) + else: + status, color = (tr_noop("OFFLINE"), Colors.DANGER) + + self._sunnylink_status.update(tr_noop("SUNNYLINK"), status, color) + + def _draw_metrics_w_sunnylink(self, rect: rl.Rectangle, _temp, _panda, _connect): + metrics = [_temp, _panda, _connect, self._sunnylink_status] + start_y = int(rect.y) + METRIC_START_Y + available_height = max(0, int(HOME_BTN.y) - METRIC_MARGIN - METRIC_HEIGHT - start_y) + spacing = available_height / max(1, len(metrics) - 1) + + return metrics, start_y, spacing From 1eb82fcc852798e6ef7dd0af7216442bdf82bd9c Mon Sep 17 00:00:00 2001 From: Nayan Date: Sun, 4 Jan 2026 00:27:22 -0500 Subject: [PATCH 5/6] ui: Global Brightness Override (#1579) * global brightness * initialize * keep stock * lint --------- Co-authored-by: Jason Wen --- selfdrive/ui/sunnypilot/ui_state.py | 3 +++ selfdrive/ui/ui_state.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 21ed78b096..79ca410e1b 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -22,6 +22,8 @@ class UIStateSP: self.sunnylink_state = SunnylinkState() + self.global_brightness_override: int = self.params.get("Brightness", return_default=True) + def update(self) -> None: if self.sunnylink_enabled: self.sunnylink_state.start() @@ -74,6 +76,7 @@ class UIStateSP: self.rainbow_path = self.params.get_bool("RainbowMode") self.chevron_metrics = self.params.get("ChevronInfo") self.active_bundle = self.params.get("ModelManager_ActiveBundle") + self.global_brightness_override = self.params.get("Brightness", return_default=True) class DeviceSP: diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index a86c84ada3..7aad769bb1 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -255,9 +255,18 @@ class Device(DeviceSP): else: clipped_brightness = ((clipped_brightness + 16.0) / 116.0) ** 3.0 - clipped_brightness = float(np.interp(clipped_brightness, [0, 1], [30, 100])) + if gui_app.sunnypilot_ui(): + if ui_state.global_brightness_override <= 0: + min_global_brightness = 1 if ui_state.global_brightness_override < 0 else 30 + clipped_brightness = float(np.interp(clipped_brightness, [0, 1], [min_global_brightness, 100])) + else: + clipped_brightness = float(np.interp(clipped_brightness, [0, 1], [30, 100])) brightness = round(self._brightness_filter.update(clipped_brightness)) + + if gui_app.sunnypilot_ui() and ui_state.global_brightness_override > 0: + brightness = ui_state.global_brightness_override + if not self._awake: brightness = 0 From e5e56614c97d147c8e05c69b60bd61b7f434dcb0 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 4 Jan 2026 00:33:32 -0500 Subject: [PATCH 6/6] ui: Customizable Interactive Timeout (#1640) * ui: Custom Interactive Timeout * rename * lint --- selfdrive/ui/sunnypilot/ui_state.py | 2 ++ selfdrive/ui/ui_state.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 79ca410e1b..35d85eca50 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -22,6 +22,7 @@ class UIStateSP: self.sunnylink_state = SunnylinkState() + self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True) self.global_brightness_override: int = self.params.get("Brightness", return_default=True) def update(self) -> None: @@ -76,6 +77,7 @@ class UIStateSP: self.rainbow_path = self.params.get_bool("RainbowMode") self.chevron_metrics = self.params.get("ChevronInfo") self.active_bundle = self.params.get("ModelManager_ActiveBundle") + self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True) self.global_brightness_override = self.params.get("Brightness", return_default=True) diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index 7aad769bb1..c7f9a7ddac 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -221,6 +221,9 @@ class Device(DeviceSP): if self._override_interactive_timeout is not None: return self._override_interactive_timeout + if gui_app.sunnypilot_ui() and ui_state.custom_interactive_timeout != 0: + return ui_state.custom_interactive_timeout + ignition_timeout = 10 if gui_app.big_ui() else 5 return ignition_timeout if ui_state.ignition else 30