diff --git a/common/params_keys.h b/common/params_keys.h index 2d4bb1a35..6a1f029ca 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -114,6 +114,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}}, @@ -261,7 +262,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/mici/onroad/torque_bar.py b/selfdrive/ui/mici/onroad/torque_bar.py index c8485a310..c1de69463 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 bcbcb2dcf..76e7b078d 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/layouts/settings/trips.py b/selfdrive/ui/sunnypilot/layouts/settings/trips.py index a318cebeb..b389892d5 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/trips.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/trips.py @@ -4,27 +4,148 @@ 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 requests +import threading +import time +import pyray as rl + +from openpilot.common.api import api_get +from openpilot.common.constants import CV from openpilot.common.params import Params -from openpilot.system.ui.widgets.scroller_tici import Scroller +from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.ui.lib.api_helpers import get_token +from openpilot.selfdrive.ui.ui_state import ui_state, device +from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID +from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget class TripsLayout(Widget): + PARAM_KEY = "ApiCache_DriveStats" + UPDATE_INTERVAL = 30 # seconds + def __init__(self): super().__init__() - self._params = Params() - items = self._initialize_items() - self._scroller = Scroller(items, line_separator=True, spacing=0) + self._session = requests.Session() + self._stats = self._get_stats() - def _initialize_items(self): - items = [ + self._icon_distance = gui_app.texture("icons/road.png", 100, 100, keep_aspect_ratio=True) + self._icon_drives = gui_app.texture("icons_mici/wheel.png", 80, 80, keep_aspect_ratio=True) + self._icon_hours = gui_app.texture("../../sunnypilot/selfdrive/assets/icons/clock.png", 80, 80, keep_aspect_ratio=True) - ] - return items + self._running = True + self._update_thread = threading.Thread(target=self._update_loop, daemon=True) + self._update_thread.start() - def _render(self, rect): - self._scroller.render(rect) + def __del__(self): + self._running = False + try: + if self._update_thread and self._update_thread.is_alive(): + self._update_thread.join(timeout=1.0) + except Exception: + pass - def show_event(self): - self._scroller.show_event() + def _get_stats(self): + stats = self._params.get(self.PARAM_KEY) + if not stats: + return {} + try: + return stats + except Exception: + cloudlog.exception(f"Failed to decode drive stats: {stats}") + return {} + + def _fetch_drive_stats(self): + try: + dongle_id = self._params.get("DongleId") + if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: + return + identity_token = get_token(dongle_id) + response = api_get(f"v1.1/devices/{dongle_id}/stats", access_token=identity_token, session=self._session) + if response.status_code == 200: + data = response.json() + self._stats = data + self._params.put(self.PARAM_KEY, data) + except Exception as e: + cloudlog.error(f"Failed to fetch drive stats: {e}") + + def _update_loop(self): + while self._running: + if not ui_state.started and device._awake: + self._fetch_drive_stats() + time.sleep(self.UPDATE_INTERVAL) + + def _render_stat_group(self, x, y, width, height, title, data, is_metric): + # Card Background + rl.draw_rectangle_rounded(rl.Rectangle(x, y, width, height), 0.05, 10, rl.Color(30, 30, 30, 255)) + + # Title + title_font = gui_app.font(FontWeight.BOLD) + rl.draw_text_ex(title_font, title, rl.Vector2(x + 60, y + 30), 50 * FONT_SCALE, 0, rl.Color(200, 200, 200, 255)) + + # Internal content area + # Center the content block (Icon + Value + Unit) vertically + content_y = y + (height / 2) - (140 * FONT_SCALE) + col_width = width / 3 + + # Values + number_font = gui_app.font(FontWeight.BOLD) + unit_font = gui_app.font(FontWeight.LIGHT) + number_base_size = 92 + unit_base_size = 55 + number_size = number_base_size * FONT_SCALE + unit_size = unit_base_size * FONT_SCALE + color_unit = rl.Color(160, 160, 160, 255) + + routes = int(data.get("routes", 0)) + distance = data.get("distance", 0) + distance_str = str(int(distance * CV.MPH_TO_KPH)) if is_metric else str(int(distance)) + hours = int(data.get("minutes", 0) / 60) + + dist_unit = tr("KM") if is_metric else tr("Miles") + + def draw_col(col_idx, icon, value, unit): + col_x = x + (col_width * col_idx) + center_x = col_x + (col_width / 2) + + # Icon + icon_x = int(center_x - (icon.width / 2)) + icon_y = int(content_y + 60) + rl.draw_texture(icon, icon_x, icon_y, rl.WHITE) + + # Value + val_size = measure_text_cached(number_font, value, number_base_size) + rl.draw_text_ex(number_font, value, rl.Vector2(center_x - val_size.x / 1.65, content_y + 145 * FONT_SCALE), number_size, 0, rl.WHITE) + + # Unit + unit_size_vec = measure_text_cached(unit_font, unit, unit_base_size) + rl.draw_text_ex(unit_font, unit, rl.Vector2(center_x - unit_size_vec.x / 1.65, content_y + 255 * FONT_SCALE), unit_size, 0, color_unit) + + draw_col(0, self._icon_drives, str(routes), tr("Drives")) + draw_col(1, self._icon_distance, distance_str, dist_unit) + draw_col(2, self._icon_hours, str(hours), tr("Hours")) + + return y + height + + def _render(self, rect: rl.Rectangle): + x = rect.x + y = rect.y + w = rect.width + + spacing = 30 + available_h = rect.height - 30 + card_height = available_h / 2 + + is_metric = self._params.get_bool("IsMetric") + + all_time = self._stats.get("all", {}) + week = self._stats.get("week", {}) + + y = self._render_stat_group(x, y, w, card_height, tr("ALL TIME"), all_time, is_metric) + y += spacing + y = self._render_stat_group(x, y, w, card_height, tr("PAST WEEK"), week, is_metric) + + return -1 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/visuals.py b/selfdrive/ui/sunnypilot/layouts/settings/visuals.py index 1036af3e5..84be5a26a 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/mici/layouts/models.py b/selfdrive/ui/sunnypilot/mici/layouts/models.py new file mode 100644 index 000000000..d5964ea96 --- /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 f6fae6630..69982e229 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) diff --git a/selfdrive/ui/sunnypilot/onroad/augmented_road_view.py b/selfdrive/ui/sunnypilot/onroad/augmented_road_view.py index 0a5739cc0..c7dedee54 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/blind_spot_indicators.py b/selfdrive/ui/sunnypilot/onroad/blind_spot_indicators.py index 1087579fe..61aa52537 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 new file mode 100644 index 000000000..8aa4c71d0 --- /dev/null +++ b/selfdrive/ui/sunnypilot/onroad/circular_alerts.py @@ -0,0 +1,140 @@ +""" +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 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) + + self._e2e_alert_display_timer = 0 + self._e2e_alert_frame = 0 + self._green_light_alert = False + self._lead_depart_alert = False + self._standstill_elapsed_time = 0.0 + self._is_standstill = 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'] + car_state = sm['carState'] + self._green_light_alert = lp_sp.e2eAlerts.greenLightAlert + self._lead_depart_alert = lp_sp.e2eAlerts.leadDepartAlert + 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 + 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 + + 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) + 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 and not (ui_state.standstill_timer and self._is_standstill)): + 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) + + # Standstill Timer (STOPPED) should be static white + 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) + + # 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 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) + + # 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) + + 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 + 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 8ca726980..d8ba4b8bf 100644 --- a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py +++ b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py @@ -6,13 +6,17 @@ 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 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 +from openpilot.selfdrive.ui.sunnypilot.onroad.circular_alerts import CircularAlertsRenderer +from openpilot.selfdrive.ui.sunnypilot.onroad.speed_renderer import SpeedRenderer class HudRendererSP(HudRenderer): @@ -22,20 +26,37 @@ 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.circular_alerts_renderer = CircularAlertsRenderer() + self.speed_renderer = SpeedRenderer() + self._torque_bar = TorqueBar(scale=3.0, always=True) def _update_state(self) -> None: super()._update_state() self.road_name_renderer.update() self.speed_limit_renderer.update() + 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) + + 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) + self.smart_cruise_control_renderer.render(rect) self.turn_signal_controller.render(rect) - - if ui_state.rocket_fuel: - self.rocket_fuel.render(rect, ui_state.sm) + self.circular_alerts_renderer.render(rect) + 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 652e620ad..f85285ef5 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 af25711a9..cb1012890 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/smart_cruise_control.py b/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py new file mode 100644 index 000000000..ca71fcac4 --- /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") diff --git a/selfdrive/ui/sunnypilot/onroad/speed_renderer.py b/selfdrive/ui/sunnypilot/onroad/speed_renderer.py new file mode 100644 index 000000000..0a017876e --- /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/onroad/turn_signal.py b/selfdrive/ui/sunnypilot/onroad/turn_signal.py index 3a66ffeb0..04fff7db7 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 f38280d49..6403157d5 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -120,18 +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.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) - - # Onroad Screen Brightness + self.developer_ui = self.params.get("DevUIInfo") + self.hide_v_ego_ui = self.params.get_bool("HideVEgoUI") 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/selfdrive/assets/icons/clock.png b/sunnypilot/selfdrive/assets/icons/clock.png new file mode 100644 index 000000000..e04d1db94 --- /dev/null +++ b/sunnypilot/selfdrive/assets/icons/clock.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e095cfc4de71788bd4a99699a2e7ab4098cd426277d672b9e43981c5fab8b40f +size 16407 diff --git a/sunnypilot/sunnylink/params_metadata.json b/sunnypilot/sunnylink/params_metadata.json index 7f597a962..c0c84eac0 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", @@ -1239,6 +1239,10 @@ "title": "Tesla Coop Steering", "description": "" }, + "TorqueBar": { + "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", "description": "" @@ -1267,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", diff --git a/system/manager/manager.py b/system/manager/manager.py index 36e45488f..8c5d35d07 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)