Merge remote-tracking branch 'origin/master' into only-chubbs

# Conflicts:
#	common/params_keys.h
#	opendbc_repo
#	selfdrive/ui/mici/onroad/model_renderer.py
#	selfdrive/ui/onroad/model_renderer.py
#	sunnypilot/modeld_v2/camera_offset_helper.py
#	sunnypilot/modeld_v2/tests/test_camera_offset_helper.py
#	sunnypilot/sunnylink/params_metadata.json
This commit is contained in:
discountchubbs
2026-01-04 08:09:26 -08:00
11 changed files with 177 additions and 57 deletions
-1
View File
@@ -221,7 +221,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
// sunnypilot model params
{"PlanplusControl", {PERSISTENT | BACKUP, FLOAT, "1.0"}},
{"CameraOffset", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
{"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
{"LagdToggleDelay", {PERSISTENT | BACKUP, FLOAT, "0.2"}},
+13 -2
View File
@@ -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:
+3 -6
View File
@@ -81,9 +81,7 @@ class ModelRenderer(Widget):
self._clip_region = None
self._counter = -1
self._camera_offset = 0.0
if camera_offset := ui_state.params.get("CameraOffset"):
self._camera_offset = float(camera_offset)
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
@@ -104,9 +102,8 @@ class ModelRenderer(Widget):
def _render(self, rect: rl.Rectangle):
sm = ui_state.sm
if self._counter % 180 == 0: # This mf runs at 60fps, so every 3 seconds...
if camera_offset := ui_state.params.get("CameraOffset"):
self._camera_offset = float(camera_offset)
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)
+2 -6
View File
@@ -57,10 +57,7 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
self._lead_vehicles = [LeadVehicle(), LeadVehicle()]
self._path_offset_z = HEIGHT_INIT[0]
self._counter = -1
self._camera_offset = 0.0
if camera_offset := ui_state.params.get("CameraOffset"):
self._camera_offset = float(camera_offset)
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)]
@@ -108,8 +105,7 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
self._path_offset_z = live_calib.height[0] if live_calib.height else HEIGHT_INIT[0]
if self._counter % 60 == 0:
if camera_offset := ui_state.params.get("CameraOffset"):
self._camera_offset = float(camera_offset)
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']:
@@ -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
+6
View File
@@ -37,6 +37,9 @@ class UIStateSP:
def on_param_change(self, param_name):
self.changed_params.add(param_name)
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:
if self.sunnylink_enabled:
self.sunnylink_state.start()
@@ -100,6 +103,9 @@ 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")
self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True)
self.global_brightness_override = self.params.get("Brightness", return_default=True)
class DeviceSP:
+13 -1
View File
@@ -219,6 +219,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
@@ -253,9 +256,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
@@ -1,3 +1,9 @@
"""
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
+1
View File
@@ -288,6 +288,7 @@ def main(demo=False):
model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay)
camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True))
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)
@@ -1,8 +1,13 @@
"""
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
@@ -31,22 +36,18 @@ class TestCameraOffset:
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
@@ -58,7 +59,7 @@ class TestCameraOffset:
result = CameraOffsetHelper.apply_camera_offset(transform, intrinsics, height, offset)
np.testing.assert_array_almost_equal(result, expected_shear)
def test_integration_update(self):
def test_update(self):
sm = MockStruct(
deviceState=MockStruct(deviceType='mici'),
roadCameraState=MockStruct(sensor='os04c10'),
@@ -66,17 +67,18 @@ class TestCameraOffset:
)
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.2)
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
+34 -31
View File
@@ -123,7 +123,7 @@
},
"CameraOffset": {
"title": "Adjust Camera Offset",
"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.",
"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,