diff --git a/common/params_keys.h b/common/params_keys.h index 2f27b14648..a559d411e4 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -221,7 +221,6 @@ inline static std::unordered_map 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"}}, 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/mici/onroad/model_renderer.py b/selfdrive/ui/mici/onroad/model_renderer.py index 5ec09279d9..0908de0bf4 100644 --- a/selfdrive/ui/mici/onroad/model_renderer.py +++ b/selfdrive/ui/mici/onroad/model_renderer.py @@ -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) diff --git a/selfdrive/ui/onroad/model_renderer.py b/selfdrive/ui/onroad/model_renderer.py index f33791c013..353cc5aa40 100644 --- a/selfdrive/ui/onroad/model_renderer.py +++ b/selfdrive/ui/onroad/model_renderer.py @@ -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']: 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 diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 6212af4fa2..71443c9939 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -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: diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index c054ab3573..46201a35a1 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -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 diff --git a/sunnypilot/modeld_v2/camera_offset_helper.py b/sunnypilot/modeld_v2/camera_offset_helper.py index 59e08f523f..7502c3eeb0 100644 --- a/sunnypilot/modeld_v2/camera_offset_helper.py +++ b/sunnypilot/modeld_v2/camera_offset_helper.py @@ -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 diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index 4233c9be8f..72e2352a99 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -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) diff --git a/sunnypilot/modeld_v2/tests/test_camera_offset_helper.py b/sunnypilot/modeld_v2/tests/test_camera_offset_helper.py index 6914502f6a..f25bcd0a35 100644 --- a/sunnypilot/modeld_v2/tests/test_camera_offset_helper.py +++ b/sunnypilot/modeld_v2/tests/test_camera_offset_helper.py @@ -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 diff --git a/sunnypilot/sunnylink/params_metadata.json b/sunnypilot/sunnylink/params_metadata.json index 4693f00264..f0ddbc9548 100644 --- a/sunnypilot/sunnylink/params_metadata.json +++ b/sunnypilot/sunnylink/params_metadata.json @@ -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,