"""StarPilot shell mounted in the existing large and compact UI stacks.""" from __future__ import annotations from openpilot.starpilot.ui.lane_change_feature import confirm_question as lane_change_question from collections import deque from dataclasses import asdict, dataclass, replace from functools import lru_cache from html import escape, unescape import os import math from pathlib import Path import time import threading from typing import Any, cast from collections.abc import Callable import pyray as rl import openpilot.cereal.messaging as messaging from opendbc.car.structs import car as car_schema from openpilot.selfdrive.ui.layouts.main import MainLayout, MainState from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout from openpilot.selfdrive.ui.ui_state import device as native_device, ui_state from openpilot.starpilot.ui.vehicle_bool import VEHICLE_BOOL_KEYS, confirmation_question as vehicle_question from openpilot.starpilot.ui.vehicle_large import SELECTION_PAGE, VehicleLarge from openpilot.starpilot.ui.clip import placed_at from openpilot.starpilot.ui.device_state import DeviceAction, DeviceRequest from openpilot.starpilot.drive_state.owner import DriveStateOwner, Rejected as DriveStateRejected from openpilot.starpilot.drive_state.evidence import PhysicalSource from openpilot.starpilot.drive_state.control import DriveStateControl, LABELS from openpilot.starpilot.storage import starpilot_storage_root from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner from openpilot.starpilot.ui.conditional_feature import confirmation_question as conditional_question from openpilot.starpilot.ui.appearance_owner import AppearanceOwner from openpilot.starpilot.ui.appearance_preferences import CameraViewChoice from openpilot.starpilot.ui.display_owner import AUTO_PREFIX as DISPLAY_AUTO_PREFIX, DisplayOwner from openpilot.starpilot.ui.display_preferences import MASTER as DISPLAY_MASTER from openpilot.starpilot.ui.developer_preview import parse_flags, preview_at from openpilot.starpilot.ui.power_owner import POWER_KEYS, PowerOwner, confirm_question, power_row_change from openpilot.starpilot.ui.pip_owner import FORMAT_PREFIX as PIP_FORMAT_PREFIX, PiPOwner, RESET as PIP_RESET from openpilot.starpilot.ui.pip_preferences import read_pip from openpilot.starpilot.ui.pip_render import PiPRenderer from openpilot.starpilot.ui.pip_sidecam import Signals, Rect from openpilot.starpilot.ui.onroad_customization import CAMERA_WIDGETS, fit_to_road, placement, widget_size from openpilot.starpilot.ui.layout_preview_sidebar import SIDEBAR_WIDTH from openpilot.starpilot.ui.pip_warning import PiPWarningSource from openpilot.starpilot.spot_monitor.preferences import read_preferences as read_vasm_preferences from openpilot.starpilot.speed_limits.vision.observation import clock_pair_ns from openpilot.starpilot.ui.sounds_owner import AUTO_PREFIX as SOUND_AUTO_PREFIX, SoundsOwner from openpilot.starpilot.ui.feature_settings_state import ( FeatureInput, FeaturePage, FeatureRow, FeatureSettingsRequest, FeatureUiAction, row_change, feature_scroll, feature_parent_page, feature_parent_title, value_request, value_editor_state, SOUND_PRESETS as SOUND_PRESETS, SYSTEM_PRESETS, sound_editor_state, ) from openpilot.starpilot.ui.lane_change_feature import KEYS as LANE_CHANGE_KEYS, RESET as LANE_CHANGE_RESET from openpilot.starpilot.models.runtime import ModelStatusSource from openpilot.starpilot.models.status import project_status from openpilot.starpilot.ui.models_state import model_page, model_profile_choices, model_profile_request, home_model_label from openpilot.starpilot.ui.maps_state import MapStatusSource from openpilot.starpilot.galaxy.access import GalaxyAccessOwner, default_owner from openpilot.starpilot.ui.galaxy_access import GalaxyAccessFlow from openpilot.starpilot.ui.home_state import HomeActionKind from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, Profile, default_font_directory from openpilot.starpilot.ui.startup import slc_action_transport_enabled, validate_artwork, validate_fonts from openpilot.starpilot.speed_limits.vision_gate import diagnostic_choice_enabled from openpilot.starpilot.ui.bluetooth_status import BluetoothStatusSource from openpilot.starpilot.ui.runtime_snapshot import RuntimeSnapshotAdapter, current_message, display_message from openpilot.starpilot.ui.runtime_software import deliver_software from openpilot.starpilot.ui.network_panel import NetworkPanelBridge from openpilot.starpilot.ui.slc_action_dispatch import SlcActionDispatcher from openpilot.starpilot.ui.onroad_state import AlertSize, BorderSignals, OnroadRequest, OnroadState, SlcUiRequest, is_personality_notice, is_lane_change_notice from openpilot.starpilot.ui.onroad_favorites import OnroadFavorites from openpilot.starpilot.ui.onroad_dm import DriverMonitorLayer from openpilot.starpilot.favorites.owner import FavoritesOwner from openpilot.starpilot.controllers.cruise_action import CruiseActionPublisher, ui_authority as cruise_action_authority from openpilot.starpilot.controllers.wheel_actions import WheelConsumer from openpilot.starpilot.system.starport.projection_bookmark import BookmarkReceiver from openpilot.starpilot.system.starport.projection_slc_action import SlcActionReceiver from openpilot.starpilot.controllers.mode_actions import ModeActionPublisher, producer_available, authority as mode_action_authority from openpilot.starpilot.favorites.actions import (BOOKMARK, CYCLE_PERSONALITY, EXPERIMENTAL, INCREASE_SPEED, DECREASE_SPEED, SET_SPEED, FORCE_COAST, PULSE_GLIDE, DISENGAGE, TRAFFIC, SWITCHBACK, SCREEN_OFF, mapped_actions) from openpilot.starpilot.favorites.state import FavoriteAction from openpilot.starpilot.favorites.layout_actions import layout_actions from openpilot.starpilot.saved_source import read_saved from openpilot.starpilot.feature_runtime import enabled as feature_enabled from openpilot.starpilot.conditional_mode.preferences import PreferenceError, SavedPreferences, decode_preferences from openpilot.starpilot.conditional_mode.policy import ModeChoice from openpilot.starpilot.conditional_mode.ui_action import ConditionalUiActionOwner from openpilot.starpilot.ui.settings_state import Destination, SettingsAction, SettingsActionKind from openpilot.starpilot.ui.shell import ShellInput, ShellMode, ShellRequest, ShellSnapshot, ShellView from openpilot.starpilot.ui.software_state import DownloadLabel, SoftwareAction, SoftwareRequest from openpilot.starpilot.ui.toggles_state import Personality, ToggleKey, ToggleRequest from openpilot.system.ui.lib.application import MouseEvent, MousePos, gui_app from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.list_view import button_item, text_item from openpilot.starpilot.ui.compact_settings_page import StarCompactSettings NATIVE_SETTINGS_PANELS = (Destination.DEVICE, Destination.SOFTWARE, Destination.BLUETOOTH, Destination.TOGGLES, Destination.DEVELOPER) _FEATURE_SETTINGS_PANES = { Destination.DRIVING_CONTROLS: ("features", "feature", "features"), Destination.SOUNDS: ("sounds", "sounds", "sounds"), Destination.APPEARANCE: ("appearance", "appearance", "appearance"), Destination.SYSTEM: ("display", "display", "display"), Destination.DRIVING_MODEL: ("models", "model", "models"), } def validate_runtime_fonts(profile: Profile) -> None: """Validate bundled fonts or an explicit override before acquiring resources.""" validate_fonts(profile, os.environ.get("STARPILOT_UI_FONT_DIR")) def validate_runtime_assets() -> None: """Reject missing or changed bundled artwork before creating app widgets.""" validate_artwork() def render_stock_confidence(camera_owner: Any, rect: rl.Rectangle, state: OnroadState) -> None: camera_owner.render_stock_confidence_layer(rect, lateral_active=state.lateral_active) @lru_cache(maxsize=1) def _slc_action_publisher() -> messaging.PubMaster: """One producer for state-bound planner actions from the UI.""" return messaging.PubMaster(["slcAction"]) def _galaxy_access_owner() -> GalaxyAccessOwner: return default_owner() @dataclass class _AolSave: context: tuple group: str cancel: threading.Event done: threading.Event saved: bool = False def visual_border_state(state: OnroadState, signals: Signals) -> OnroadState: border = state.border_signals if border is None or not (signals.vasm_left or signals.vasm_right): return state return replace(state, border_signals=BorderSignals( border.left_blinker, border.right_blinker, border.left_blindspot or signals.vasm_left, border.right_blindspot or signals.vasm_right)) class StarShellSession: """Own native shell resources and one request/router boundary per app.""" def __init__(self, profile: Profile, camera_owner: Any, *, network_layer: Any = None, settings_layer: Any = None): font_dir = os.environ.get("STARPILOT_UI_FONT_DIR") self.profile = profile self.network_layer = network_layer self.settings_layer = settings_layer self.fonts = BitmapFonts(profile, Path(font_dir) if font_dir else default_font_directory()) self.pip_renderer = PiPRenderer("bubble") self.pip_warning = PiPWarningSource() self._pip_saved = None self._vasm_saved = None self._pip_read_ns: int | None = None self._pip_signals_cache = None try: self.camera_owner = camera_owner self.camera_paint = True def camera_layer(rect, state): if profile == Profile.COMPACT: camera_owner.render_camera_model_layer(rect, camera_view=state.appearance.camera_view, reverse_driver=state.reverse_driver_camera, lead_indicator=state.appearance.show_lead_indicator and state.lead_indicator_source_fresh, lead_info_mode=state.appearance.lead_info_mode, lead_info_metric=state.appearance.lead_info_metric, lateral_active=state.lateral_active, road_style=state.customization["roadColors"]["compact"], paint=self.camera_paint) else: camera_owner.render_camera_model_layer(rect, camera_view=state.appearance.camera_view, reverse_driver=state.reverse_driver_camera, road_style=state.customization["roadColors"]["large"]) self.view = ShellView(self.fonts, Path(__file__).parents[2] / "selfdrive/assets", camera_layer=camera_layer, pip_layer=self._render_pip) self.view.onroad.driver_monitor_layer = self._render_driver_monitor self._driver_monitor_layer = None if profile == Profile.COMPACT: self.view.onroad.stock_confidence_layer = lambda rect, state: render_stock_confidence(camera_owner, rect, state) self.view.onroad.stock_confidence_reset = camera_owner.reset_stock_confidence_layer self.view.onroad.extra_overlays = lambda rect, state: camera_owner.render_model_source_layer(rect, show_long_indicator=None) self.view.onroad.compact_sidebar.personality_renderer = self._render_personality else: self.view.onroad.extra_overlays = lambda rect, state: camera_owner.render_model_source_layer(rect) except Exception: self.fonts.close() raise self.galaxy_access = _galaxy_access_owner() self.bluetooth_source = BluetoothStatusSource() if profile == Profile.COMPACT else None self.adapter = RuntimeSnapshotAdapter(ui_state, self.galaxy_access, self.bluetooth_source.snapshot if self.bluetooth_source is not None else None) self.galaxy_flow = GalaxyAccessFlow(self.galaxy_access, self.confirmed_offroad) self.feature_owner = FeatureSettingsOwner(ui_state.params, self._feature_authority, vehicle_fingerprint=lambda: getattr(ui_state.CP, "carFingerprint", None), vehicle_params=lambda: ui_state.CP, vision_development=lambda: diagnostic_choice_enabled(os.environ, ui_state.CP)) self.sounds_owner = SoundsOwner(ui_state.params, self._settings_preference_authority) self.appearance_owner = AppearanceOwner(ui_state.params, lambda: self._settings_preference_authority() or self._favorite_authority()) self.pip_owner = PiPOwner(ui_state.params, self._settings_preference_authority, self.pip_renderer.frame_availability.available) self.appearance_page = "appearance" self.display_owner = DisplayOwner(ui_state.params, self._settings_preference_authority) self.model_source: ModelStatusSource | None = None self._home_model: tuple[float, str] | None = None self.map_source: MapStatusSource | None = None self.model_scroll = 0 self.drive_state = DriveStateControl(DriveStateOwner(ui_state.params, starpilot_storage_root() / "drive-state"), PhysicalSource(ui_state.sm)) self.system_power_action: Callable[[int], None] | None = None self.power_owner = PowerOwner(ui_state.params, self._settings_preference_authority) self._power_request_epoch = 0 self._pip_request_epoch = 0 self._lane_change_request_epoch = 0 self.feature_page: str = FeaturePage.HUB self.feature_root_page: str = FeaturePage.HUB self.feature_scroll = 0 self.feature_edit_key: str | None = None self.sounds_scroll = 0 self.sounds_edit_key: str | None = None self.appearance_scroll = 0 self.appearance_edit_key: str | None = None self.display_scroll = 0 self.display_page = "system" self.display_edit_key: str | None = None self.slc_actions: SlcActionDispatcher | None = None if slc_action_transport_enabled(os.environ, ui_state.params): try: self.slc_actions = SlcActionDispatcher(_slc_action_publisher(), self._live_slc_message) except (OSError, RuntimeError): pass # The shell stays readable and reports action unavailability. self.input = ShellInput(profile, self._emit, native_panels=NATIVE_SETTINGS_PANELS if profile == Profile.LARGE and settings_layer is not None else ()) if profile == Profile.LARGE: self.view.onroad.pressed_action = lambda: self.input.onroad.pressed_action self.selected = Destination.STAR self.sidebar_expanded = True self._onroad_page_width = 2160.0 self.compact_y = 0.0 self.compact_scroll_x = 0.0 self.notice = "" self.notice_until = 0.0 self._mode = ShellMode.HOME self._on_settings: Any = None self._on_home: Any = None self._on_pairing: Any = None self._on_compact_destination: Any = None self._on_destination_change: Any = None self._request_owner: Any = None self._request_emitted = False self._snapshot_cache: tuple[tuple, ShellSnapshot] | None = None self._rendered_settings: tuple[int, ShellSnapshot] | None = None self._settings_touch: ShellSnapshot | None = None self._visual_preview_flags = frozenset() raw_preview = os.environ.get("SP_ONROAD_VISUAL_PREVIEW", "") if raw_preview: from openpilot.tools.replay.onroad import PREFIX_RE if (os.environ.get("SP_HOST_RUNTIME") == "1" and PREFIX_RE.fullmatch(os.environ.get("OPENPILOT_PREFIX", "")) is not None): self._visual_preview_flags = parse_flags(raw_preview) self._visual_preview_start_ns = time.monotonic_ns() self._native_favorite_actions: Callable[[], dict] = dict # Native publisher for taps on the projected car screen. self._publish_bookmark: Callable[[], None] | None = None self._wheel_sock = messaging.sub_sock("slcCruiseEvent", conflate=False) self._aol_save = None self._aol_save_closed = False self._wheel_pending = deque(maxlen=32) self._wheel_consumer = WheelConsumer() self._wheel_personality = None self.conditional_actions: ConditionalUiActionOwner | None = None self.cruise_actions = CruiseActionPublisher() self.favorites_owner = FavoritesOwner(ui_state.params, self._favorite_actions, self._favorite_authority, self._favorite_render_actions, current_action=lambda key: self._resolve_favorite_actions(frozenset((key,))).get(key)) self.favorites = OnroadFavorites(self.fonts, self._favorite_request) self._favorite_snapshot = None self._favorite_data = None self._favorite_read_at = None self._favorite_include_options = False self._favorite_claimed = False self._projection_bookmarks = None self._projection_slc = None if getattr(ui_state, 'replay_clock', None) is None: try: self._projection_bookmarks = BookmarkReceiver() except OSError: pass # Projection bookmarks stay unavailable without affecting the native UI. try: self._projection_slc = SlcActionReceiver() except OSError: pass # Car-screen speed-limit taps stay unavailable without affecting the native UI. self.onroad_map = self.onroad_jetlink = None if profile == Profile.LARGE: from openpilot.starpilot.ui.onroad_map_layer import NativeMapLayer self.onroad_map = NativeMapLayer(self.fonts, ui_state) self.view.onroad.map_layer = self.onroad_map.render from openpilot.starpilot.ui.onroad_jetlink_layer import NativeJetlinkLayer self.onroad_jetlink = NativeJetlinkLayer(self.view.onroad.jetlink_status, ui_state) self.view.onroad.jetlink_status = self.onroad_jetlink def _favorite_authority(self, *, allow_personality_notice: bool = False) -> bool: if getattr(ui_state, "replay_clock", None) is not None: return False if getattr(self, "_projection_tap", False): return bool(ui_state.started and display_message(ui_state.sm, 'carState', time.monotonic_ns(), after_frame=ui_state.started_frame) is not None) if self._mode != ShellMode.ONROAD: return False state = self.snapshot(ShellMode.ONROAD).onroad camera_allowed = (self.profile != Profile.COMPACT or not state.reversing or state.appearance.camera_view in (CameraViewChoice.NONE, CameraViewChoice.DRIVER)) alert_allowed = state.alert.size == AlertSize.NONE or allow_personality_notice and is_personality_notice(state.alert) return bool(self._mode == ShellMode.ONROAD and state.camera_available and alert_allowed and camera_allowed) def _render_driver_monitor(self, rect, state): from openpilot.starpilot.ui.runtime_snapshot import display_message now = state.observed_ns or time.monotonic_ns() monitor = display_message(ui_state.sm, "driverMonitoringState", now, after_frame=ui_state.started_frame) driver = display_message(ui_state.sm, "driverStateV2", now, after_frame=ui_state.started_frame) if self._driver_monitor_layer is None: self._driver_monitor_layer = DriverMonitorLayer(self.profile) top_icons = (self.profile == Profile.COMPACT and state.alert.size == AlertSize.NONE and state.appearance.camera_view != CameraViewChoice.DRIVER and not state.reverse_driver_camera and self.view.onroad.compact_hud._set_speed_alpha.x > 0.01 and state.customization["layouts"]["compact"]["max_speed"]["enabled"]) self._driver_monitor_layer.render(state, monitor=monitor, driver=driver, fresh=monitor is not None and driver is not None, onroad=ui_state.is_onroad(), top_icons=top_icons) def _render_personality(self, rect, state): indicator = self.camera_owner._long_indicator indicator.set_should_draw(state.alert.size == AlertSize.NONE or is_lane_change_notice(state.alert)) indicator.render_sidebar(rect, personality=state.personality, longitudinal_active=state.cruise_active, traffic_mode=state.traffic_mode) def _favorite_actions(self): return self._resolve_favorite_actions() def _favorite_render_actions(self, keys): return self._resolve_favorite_actions(keys) def _resolve_favorite_actions(self, keys=None): actions = mapped_actions(lambda page: self.feature_snapshot(page, favorite=True), self.feature_request, lambda: self.appearance_owner.snapshot(self.profile), self.appearance_owner.apply, requested_keys=keys) actions.update(layout_actions(ui_state.params, str(self.profile), lambda: self._settings_preference_authority() or self._favorite_authority(), lambda: self.adapter.invalidate_appearance())) actions.update(self._native_favorite_actions()) return actions if keys is None else {key: action for key, action in actions.items() if key in keys} def native_favorite_actions(self, bookmark, personality, experimental): state = self.snapshot(ShellMode.ONROAD).onroad available = self._favorite_authority() conditional = self._conditional_favorite_active() long_available = bool(available and ui_state.CP is not None and ui_state.has_longitudinal_control and not ui_state.params.get_bool("SafeMode")) drive = (ui_state.started_frame, getattr(ui_state.CP, "carFingerprint", None), getattr(ui_state.CP, "flags", None)) current_personality = int(ui_state.personality) def guarded(callback): return self._favorite_authority() and callback() def guarded_long(callback): return bool(self._favorite_authority() and ui_state.CP is not None and ui_state.has_longitudinal_control and not ui_state.params.get_bool("SafeMode") and callback()) context = (self._conditional_action_owner().context(ui_state.sm, ui_state.CP, now_ns=time.monotonic_ns()) if long_available and conditional else None) exp_available = bool(long_available and (not conditional or context is not None)) def toggle_experimental(): if self._conditional_favorite_active() != conditional: return False if conditional: return bool(context is not None and guarded_long(lambda: self._conditional_action_owner().dispatch( context, ui_state.sm, ui_state.CP, _slc_action_publisher(), now_ns=time.monotonic_ns()))) return guarded_long(experimental) def cycle_personality(direction): if not (self._favorite_authority(allow_personality_notice=True) and ui_state.CP is not None and ui_state.has_longitudinal_control and not ui_state.params.get_bool("SafeMode")): return False try: raw, readable = read_saved(ui_state.params, "LongitudinalPersonality", 8) except (AttributeError, OSError, TypeError, ValueError): return False if not readable or raw not in (None, b"0", b"1", b"2"): return False current = int(raw) if raw is not None else 1 # Factory Standard when the setting is absent. return personality((current + direction) % 3) personality_available = bool(self._favorite_authority(allow_personality_notice=True) and ui_state.CP is not None and ui_state.has_longitudinal_control and not ui_state.params.get_bool("SafeMode")) self._wheel_personality = lambda: cycle_personality(-1) actions = { BOOKMARK: FavoriteAction(BOOKMARK, "Bookmark", available=available, reason="" if available else "Onroad only", token=repr((drive, available)), invoke=lambda: guarded(bookmark)), CYCLE_PERSONALITY: FavoriteAction(CYCLE_PERSONALITY, "Cycle Driving Personality", kind="enum", state_label=("Aggressive", "Standard", "Relaxed")[current_personality], available=personality_available, reason="" if personality_available else "Longitudinal control required", token=repr((drive, current_personality, personality_available)), invoke=lambda: cycle_personality(1)), EXPERIMENTAL: FavoriteAction(EXPERIMENTAL, "Experimental Mode", kind="toggle", state_label=("Overridden Chill" if (state.conditional_effective is not None and state.conditional_effective.reason == 'manual_chill') else "Forced Experimental" if (state.conditional_effective is not None and state.conditional_effective.reason == 'manual_experimental') else "Automatic Experimental" if (state.conditional_effective is not None and state.conditional_effective.effective_experimental) else "On" if state.experimental_enabled else "Off"), available=exp_available, reason=("Active longitudinal control and current conditional status required" if conditional else "Confirmation and longitudinal control required"), token=repr((drive, state.experimental_enabled, exp_available, conditional, context.token if context is not None else None)), invoke=toggle_experimental), } from openpilot.starpilot.controllers.selfie import SelfieCapture, authority as selfie_authority from openpilot.starpilot.controllers.wheel_actions import cp_fingerprint, eligible from openpilot.starpilot.favorites.actions import SELFIE if not hasattr(self, 'selfie_capture'): self.selfie_capture = SelfieCapture() selfie_drive = int(getattr(ui_state.sm['deviceState'], 'startedMonoTime', 0)) selfie_fingerprint = cp_fingerprint(ui_state.CP) if eligible(ui_state.CP) else '' def selfie_permitted(): return getattr(self, "_mode", None) == ShellMode.ONROAD and selfie_authority(ui_state.sm, ui_state.CP, drive_id=selfie_drive, fingerprint=selfie_fingerprint, now_ns=time.monotonic_ns()) selfie_allowed = available and selfie_permitted() and not self.selfie_capture.busy actions[SELFIE] = FavoriteAction(SELFIE, "Selfie", available=selfie_allowed, reason="Current cabin camera and drive required" if not selfie_allowed else "", token=repr((selfie_drive, selfie_fingerprint, selfie_allowed)), invoke=lambda: self._favorite_authority() and self.selfie_capture.submit(selfie_permitted)) from openpilot.selfdrive.ui.ui_state import device actions[SCREEN_OFF] = FavoriteAction(SCREEN_OFF, "Toggle Screen Off", kind="toggle", state_label="Off" if device.awake else "On", available=available, reason="Onroad only" if not available else "", token=repr((drive, available, device.awake)), invoke=lambda: guarded(device.toggle_screen_off)) if not hasattr(self, 'mode_actions'): self.mode_actions = ModeActionPublisher() for key, label, mode, requested in ((TRAFFIC, "Traffic Mode", 'traffic', state.traffic_mode), (SWITCHBACK, "Switchback Mode", 'switchback', getattr(state, 'switchback_mode', False))): now_ns = time.monotonic_ns() allowed = (self._favorite_authority() and not ui_state.params.get_bool("SafeMode") and mode_action_authority(ui_state.sm, ui_state.CP, now_ns, mode) and producer_available(ui_state.sm, now_ns=now_ns)) def toggle_mode(mode=mode): return bool(self._favorite_authority() and not ui_state.params.get_bool("SafeMode") and self.mode_actions.dispatch( mode, ui_state.sm, ui_state.CP, _slc_action_publisher(), now_ns=time.monotonic_ns())) actions[key] = FavoriteAction(key, label, kind="toggle", state_label="On" if requested else "Off", available=allowed, reason="Current qualified control required" if not allowed else "", token=repr((drive, mode, requested, allowed)), invoke=toggle_mode) for key, label, mode in ((FORCE_COAST, "Force Coasting", "coast"), (PULSE_GLIDE, "Pulse and Glide", "pulse"), (DISENGAGE, "Disengage Openpilot", "disengage")): now_ns = time.monotonic_ns() allowed = (self._favorite_authority() and mode_action_authority(ui_state.sm, ui_state.CP, now_ns, mode) and (mode == "disengage" or not ui_state.params.get_bool("SafeMode") and producer_available(ui_state.sm, now_ns=now_ns))) def apply_mode(mode=mode): return bool(self._favorite_authority() and (mode == "disengage" or not ui_state.params.get_bool("SafeMode")) and self.mode_actions.dispatch(mode, ui_state.sm, ui_state.CP, _slc_action_publisher(), now_ns=time.monotonic_ns())) actions[key] = FavoriteAction(key, label, available=allowed, token=repr((drive, mode, allowed)), reason="Fresh onroad control required", invoke=apply_mode) if ui_state.CP is not None and not ui_state.CP.pcmCruise: current = self._favorite_authority() and cruise_action_authority(ui_state.sm, ui_state.CP, time.monotonic_ns()) def change_speed(increase): return bool(not ui_state.params.get_bool("SafeMode") and self.cruise_actions.dispatch( increase, ui_state.sm, ui_state.CP, _slc_action_publisher(), now_ns=time.monotonic_ns())) for key, label, increase in ((INCREASE_SPEED, "Increase Speed", True), (DECREASE_SPEED, "Decrease Speed", False)): actions[key] = FavoriteAction(key, label, available=current and not ui_state.params.get_bool("SafeMode"), reason="Active software cruise control required", token=repr((drive, current)), invoke=lambda increase=increase: change_speed(increase)) def set_speed(value): if not self._favorite_authority() or type(value) not in (int, float) or not math.isfinite(value): return False metric_raw, readable = read_saved(ui_state.params, "IsMetric", 8) if not readable or metric_raw not in (None, b"0", b"1"): return False metric = metric_raw == b"1" if not (8 <= value <= 145 if metric else 5 <= value <= 90): return False return bool(not ui_state.params.get_bool("SafeMode") and self.cruise_actions.dispatch( True, ui_state.sm, ui_state.CP, _slc_action_publisher(), now_ns=time.monotonic_ns(), target_mps=float(value) * (1 / 3.6 if metric else 0.44704))) actions[SET_SPEED] = FavoriteAction(SET_SPEED, "Set Speed To", available=current and not ui_state.params.get_bool("SafeMode"), reason="Active software cruise required; speed uses device units", token=repr((drive, current)), invoke_value=set_speed) return actions def _poll_projection_bookmark(self, now_ns): receiver = getattr(self, '_projection_bookmarks', None) if receiver is None: return # Bookmark taps use live onroad state, independent of this screen's page or alerts. # A render gap beyond the 20 ms control limit must not lose drained requests. publish = getattr(self, '_publish_bookmark', None) authorized = bool(publish is not None and ui_state.started and getattr(ui_state, "replay_clock", None) is None and display_message(ui_state.sm, 'carState', now_ns, after_frame=ui_state.started_frame) is not None) stamp = int(ui_state.sm.logMonoTime['carState']) if authorized else 0 for _ in range(receiver.drain(now_ns, stamp, authorized=authorized)): cast('Callable[[], None]', publish)() def _poll_projection_slc(self, now_ns): receiver = getattr(self, '_projection_slc', None) if receiver is None: return # Always drain; publish only when the live-state tap checks pass. for request in receiver.drain(now_ns): if not ui_state.started or getattr(ui_state, "replay_clock", None) is not None: continue if isinstance(request, OnroadRequest): self._projection_experimental(request.value) elif self.slc_actions is not None: self.slc_actions.dispatch(request) def _projection_experimental(self, requested: bool) -> None: """A car-screen wheel tap: the native experimental binding acts only if it is available and still in the state the car screen showed (the opposite of requested).""" self._projection_tap = True try: binding = self._native_favorite_actions().get(EXPERIMENTAL) current = self.snapshot(ShellMode.ONROAD).onroad.experimental_enabled if not (binding is not None and binding.available and binding.invoke is not None and current != requested and binding.invoke()): self._unavailable("experimental shortcut in this context") finally: self._projection_tap = False def _poll_wheel(self, now_ns): if getattr(ui_state, "replay_clock", None) is not None: return for _ in range(32): event = messaging.recv_one_or_none(self._wheel_sock) if event is None: break if event.valid and str(event.slcCruiseEvent.kind) == "wheelAction": self._wheel_pending.append(event) for _ in range(len(self._wheel_pending)): event = self._wheel_pending.popleft() wire = event.slcCruiseEvent.wheelAction if (int(wire.sourceCarStateMonoTime) > int(ui_state.sm.logMonoTime["carState"]) or int(wire.sourceCarControlMonoTime) > int(ui_state.sm.logMonoTime["carControl"])) and now_ns <= int(wire.validUntilMonoTime): self._wheel_pending.append(event) continue command = self._wheel_consumer.accept(event, ui_state.params, ui_state.CP, ui_state.sm, now_ns=now_ns) if command is None or command.action == 0 or not self._favorite_authority(allow_personality_notice=command.action == 1): continue actions = self._native_favorite_actions() try: if command.action == 1: if self._wheel_personality is not None: self._wheel_personality() elif command.action in (5, 7, 8): key = {5: EXPERIMENTAL, 6: TRAFFIC, 7: SWITCHBACK, 8: BOOKMARK}[command.action] action = actions.get(key) if action is not None and action.available and action.invoke is not None: action.invoke() elif command.action == 10 and self.slc_actions is not None: from openpilot.starpilot.ui.onroad_state import SlcActionKind state = self._live_slc_message(now_ns) if state is not None and not state.hasPending: self.slc_actions.dispatch(SlcUiRequest(SlcActionKind.ADOPT, str(state.sessionId), int(state.decisionId), int(state.presentationId), float(state.speedLimit))) elif command.action in (11, 12, 13): slot = self.favorites_owner.snapshot().slots[command.action - 11] if slot.available and slot.request is not None: self.favorites_owner.invoke(slot.request) except (OSError, RuntimeError, ValueError, TypeError): pass def _conditional_action_owner(self): if self.conditional_actions is None: self.conditional_actions = ConditionalUiActionOwner(ui_state.params) return self.conditional_actions def _conditional_favorite_active(self): if not feature_enabled(ui_state.params, ui_state.CP, "conditional", os.environ): return False try: raw = ui_state.params.get("ConditionalModeConfig") preferences = SavedPreferences() if raw is None else decode_preferences(raw) return preferences.mode != ModeChoice.STOCK except (PreferenceError, OSError, TypeError, ValueError): return True def _update_favorites(self, state, now): include_options = self.favorites.mode == "picker" if (self._favorite_read_at is None or now < self._favorite_read_at or now - self._favorite_read_at >= 1 or include_options and not getattr(self, "_favorite_include_options", False)): self._favorite_snapshot = self.favorites_owner.snapshot(include_options=include_options) self._favorite_data = asdict(self._favorite_snapshot) self._favorite_include_options = include_options self._favorite_read_at = now self.favorites.update(state, self._favorite_data, now) def _favorite_request(self, request) -> bool: if not self._favorite_authority(): return False snapshot = self._favorite_snapshot if snapshot is None or snapshot.revision != request.revision: return False if request.kind == "activate": slot = snapshot.slots[request.index] success = slot.request is not None and self.favorites_owner.invoke(slot.request).success elif request.kind == "assign": success = self.favorites_owner.configure_index(request.index, request.key, request.revision) elif request.kind == "unassign": success = self.favorites_owner.clear_index(request.index, request.revision) else: return False if success: include_options = request.kind != "activate" self._favorite_snapshot = self.favorites_owner.snapshot(include_options=include_options) self._favorite_data = asdict(self._favorite_snapshot) self._favorite_include_options = include_options self.favorites.data = self._favorite_data self._favorite_read_at = time.monotonic() else: self._favorite_read_at = None return bool(success) def set_navigation(self, *, on_settings: Any, on_home: Any, on_pairing: Any = None, on_compact_destination: Any = None, on_destination_change: Any = None) -> None: self._on_settings = on_settings self._on_home = on_home self._on_pairing = on_pairing self._on_compact_destination = on_compact_destination self._on_destination_change = on_destination_change def set_request_owner(self, owner: Any) -> None: self._request_owner = owner def confirmed_offroad(self) -> bool: return bool(ui_state.is_offroad() and self.adapter.confirmed_offroad()) def connectivity_allowed(self) -> bool: # Connectivity setup also works on USB power without vehicle telemetry. return ui_state.is_offroad() def configuration_allowed(self) -> bool: drive_state = getattr(self, 'drive_state', None) return self.confirmed_offroad() or bool(drive_state is not None and drive_state.physical.allowed()) def _settings_preference_authority(self) -> bool: return self._mode == ShellMode.SETTINGS def _feature_authority(self, group: str) -> bool: preference_authority = self._settings_preference_authority() or ( group in ("preferences", "lane", "long", "toyota_cruise", "long_output", "slc", "aol") and self._favorite_authority()) if group == "preferences": return preference_authority if group == "parked_preferences": return self._settings_preference_authority() and self.configuration_allowed() cp = ui_state.CP if cp is None or not getattr(cp, "carFingerprint", ""): return False if group in ("lane_change", "conditional", "conditional_wheel", "switchback_wheel", "aol_wheel", "lane", "long", "toyota_cruise", "long_output", "slc", "torque", "aol", "vehicle", "toyota_radar"): if not preference_authority: return False if group == "toyota_cruise": from openpilot.starpilot.controllers.toyota_cruise import capability return capability(cp) is not None if group == "toyota_radar": from openpilot.starpilot.car.toyota.radar_preferences import capability return capability(cp) is not None if group == "vehicle": from opendbc.car.toyota.interface import toyota_auto_hold_supported return toyota_auto_hold_supported(cp) if group == "lane_change": return not cp.notCar and not cp.passive and not cp.dashcamOnly if group == "switchback_wheel": from openpilot.starpilot.feature_capabilities import lateral_supported return lateral_supported(cp) if group in ("conditional", "conditional_wheel", "long_output", "slc"): return bool(cp.openpilotLongitudinalControl and not cp.notCar and not cp.passive and not cp.dashcamOnly) if group in ("aol_wheel", "lane", "torque", "aol"): return not cp.notCar and not cp.passive and not cp.dashcamOnly return bool(cp.openpilotLongitudinalControl and not cp.notCar and not cp.passive and not cp.dashcamOnly) return False def feature_snapshot(self, page: str | None = None, *, favorite: bool = False): self._poll_aol_save() state = self.feature_owner.snapshot(page or self.feature_page, parked=False if favorite else self.configuration_allowed(), system_long=self._feature_authority("long"), lateral_context=self._feature_authority("lane"), metric=bool(ui_state.is_metric), configure_while_driving=self._settings_preference_authority() or (favorite and self._favorite_authority())) if self.profile == Profile.LARGE and state.page == FeaturePage.VEHICLE: state = replace(state, rows=self._vehicle_selector().rows() + state.rows, subtitle="Vehicle selection applies at the next startup.") if getattr(self, "_aol_save", None) is not None: state = replace(state, rows=tuple(replace(row, available=False, reason="Saving AOL preference…") if row.key == "AlwaysOnLateral" else row for row in state.rows)) if self.profile == Profile.LARGE and not favorite: state = value_editor_state(state, getattr(self, "feature_edit_key", None) if page is None else None) if state.editor and state.rows[0].key in (*LANE_CHANGE_KEYS, "CustomCruise", "CustomCruiseLong", "ToyotaRadarMode"): state = replace(state, save_hint="Changes require confirmation") return state def _vehicle_selector(self) -> VehicleLarge: if getattr(self, "vehicle_selector", None) is None: self.vehicle_selector = VehicleLarge(self) return self.vehicle_selector def _aol_context(self): return self._mode, getattr(self, "selected", None), getattr(self, "feature_page", None) def _poll_aol_save(self) -> None: pending = getattr(self, "_aol_save", None) if pending is None: return if self._aol_context() != pending.context or not self._feature_authority(pending.group): pending.cancel.set() if not pending.done.is_set(): self.notice, self.notice_until = "Saving AOL preference…", time.monotonic() + 3.0 return self._aol_save = None self._snapshot_cache = None self._favorite_read_at = None if pending.saved: self.notice, self.notice_until = "AOL preference saved", time.monotonic() + 3.0 else: self._unavailable("AOL preference was not saved; refresh settings") def _save_aol(self, request: FeatureSettingsRequest) -> bool: self._poll_aol_save() if getattr(self, "_aol_save_closed", False) or getattr(self, "_aol_save", None) is not None: return False group = "preferences" if request.value == "Off" else "aol" if not self._feature_authority(group): self._unavailable("AOL preference is unavailable in this context") return False pending = _AolSave(self._aol_context(), group, threading.Event(), threading.Event()) self._aol_save = pending owner = self.feature_owner def authorized(group: str) -> bool: return (group == pending.group and not pending.cancel.is_set() and not getattr(self, "_aol_save_closed", False) and self._aol_context() == pending.context) writer = FeatureSettingsOwner(owner.params, authorized, vehicle_fingerprint=owner.vehicle_fingerprint, vehicle_params=owner._vehicle_params_source) def save() -> None: try: pending.saved = writer.apply(request) except Exception: pending.saved = False finally: pending.done.set() threading.Thread(target=save, name="aol-preference", daemon=True).start() self._snapshot_cache = None self.notice, self.notice_until = "Saving AOL preference…", time.monotonic() + 3.0 return False # The bool API reports a completed save, never an accepted job. def feature_request(self, request: FeatureSettingsRequest) -> bool: if request.key == "AlwaysOnLateral": return self._save_aol(request) ok = self.feature_owner.apply(request) self._snapshot_cache = None if not ok: self._unavailable("Saved conditional choice may have changed; refresh settings" if request.key.startswith("conditional:") else "Curve learning was not verified; refresh saved settings" if request.key in ("curve_reset",) else "Saved setting changed or is unavailable; refresh settings") return ok def sounds_snapshot(self): state = self.sounds_owner.snapshot() if self.profile == Profile.LARGE: rows = tuple(row for row in state.rows if not row.key.startswith(SOUND_AUTO_PREFIX)) return sound_editor_state(replace(state, rows=rows), self.sounds_edit_key) return state def sounds_request(self, request: FeatureSettingsRequest) -> bool: ok = self.sounds_owner.apply(request) self._snapshot_cache = None if not ok: self._unavailable("saved sound level changed or settings closed") return ok def appearance_snapshot(self): state = self.pip_snapshot() if getattr(self, "appearance_page", "appearance") == "pip" else self.appearance_owner.snapshot(self.profile) return value_editor_state(state, getattr(self, "appearance_edit_key", None)) if self.profile == Profile.LARGE else state def pip_snapshot(self): return self.pip_owner.snapshot() def appearance_request(self, request: FeatureSettingsRequest) -> bool: pip_request = request.key.startswith(("pip:", "PIPPreview")) ok = self.pip_owner.apply(request) if pip_request else self.appearance_owner.apply(request) if ok: self.adapter.invalidate_appearance() self._pip_read_ns = None self._snapshot_cache = None if not ok: self._unavailable("saved side-camera choice may have changed; refresh settings" if pip_request and self.pip_owner.last_write.committed else "saved display choice changed or settings closed") return ok def _pip_signals(self): now_ns = time.monotonic_ns() if (self._pip_read_ns is None or now_ns < self._pip_read_ns or now_ns - self._pip_read_ns >= 1_000_000_000): self._pip_saved = read_pip(ui_state.params) self._vasm_saved = read_vasm_preferences(ui_state.params) self._pip_read_ns = now_ns car = display_message(ui_state.sm, "carState", now_ns, after_frame=ui_state.started_frame) vasm = self._vasm_saved pair = clock_pair_ns() if car is not None else None vasm_enabled = (car is not None and car.canValid and car.gearShifter == car_schema.CarState.GearShifter.drive and pair is not None and vasm is not None and vasm.readable and vasm.valid and vasm.preferences.enabled) left_warning, right_warning = self.pip_warning.sample( enabled=vasm_enabled, settings_fingerprint=vasm.fingerprint if vasm_enabled else "", now_mono_ns=pair[0] if pair is not None else 0, now_boot_ns=pair[1] if pair is not None else 0) return Signals(car is not None, bool(car.leftBlinker) if car is not None else False, bool(car.rightBlinker) if car is not None else False, bool(car.leftBlindspot) if car is not None else False, bool(car.rightBlindspot) if car is not None else False, left_warning, right_warning) def _render_pip(self, rect: rl.Rectangle, state, *, submit=None) -> None: signals = getattr(self, "_pip_signals_cache", None) or self._pip_signals() saved = self._pip_saved if (saved is None or saved.enabled is not True or saved.mask is None or saved.invert is None or saved.on_blinker is None or saved.on_bsm is None): self.pip_renderer.deactivate() return camera_positions = {} for side, key in zip(("left", "right"), CAMERA_WIDGETS, strict=True): position = placement(state.customization, self.profile, key) if position["enabled"]: width, height = widget_size(state.customization, self.profile, key) camera_positions[side] = Rect(position["x"], position["y"], width, height) status = self.pip_renderer.render(rect, saved.mask, signals, enabled=True, on_blinker=saved.on_blinker, on_bsm=saved.on_bsm, invert=saved.invert, placements=camera_positions, **({"submit": submit} if submit is not None else {})) if status not in ("rendered", "inactive") and state.alert.size == AlertSize.NONE: self.fonts.draw("SIDE CAMERA UNAVAILABLE", FontRole.MEDIUM, 18 if self.profile == Profile.COMPACT else 25, 18 if self.profile == Profile.COMPACT else rect.x + 30, 180 if self.profile == Profile.COMPACT else rect.y + rect.height - 62, rl.Color(245, 200, 155, 255)) def display_snapshot(self): return self.display_owner.snapshot(self.profile) def power_snapshot(self): return self.power_owner.snapshot() def system_snapshot(self): display = self.display_snapshot() power = self.power_snapshot() maps = self.map_snapshot() state = replace(display, title="System", subtitle="Display, drive state, power, and offline maps.", rows=display.rows + power.rows + maps.rows) if self.profile == Profile.LARGE: display_rows = tuple(replace(row, presets=SYSTEM_PRESETS[row.key], page=row.key) if row.key in SYSTEM_PRESETS else row for row in display.rows if not row.key.startswith(DISPLAY_AUTO_PREFIX)) power_rows = tuple(replace(row, presets=SYSTEM_PRESETS[row.key], page=row.key) if row.key in SYSTEM_PRESETS else row for row in power.rows) power_rows = value_editor_state(replace(power, rows=power_rows), None).rows if getattr(self, "display_edit_key", None) is not None: row = next(row for row in display_rows + power_rows if row.key == self.display_edit_key) return replace(state, page=getattr(self, "display_page", "system"), title=row.label, subtitle=row.reason, rows=(replace(row, page=""),), parent_title="System" if row.key in POWER_KEYS else "Custom Display Settings", editor=True, save_hint="Changes require confirmation" if row.key in POWER_KEYS else state.save_hint) if getattr(self, "display_page", "system") == "display": return replace(display, title="Custom Display Settings", subtitle="Brightness and screen timing.", rows=display_rows, parent_title="System") drive = self.drive_state.snapshot() drive_row = FeatureRow("drive_state", "Force Drive State", LABELS[drive["mode"]], source=drive["revision"].encode() if drive["revision"] is not None else None, available=drive["available"], reason=f"Device: {drive['effective'] or 'unavailable'}", actions=tuple((label, drive["mode"] != mode) for mode, label in LABELS.items())) power_available = getattr(self, "system_power_action", None) is not None and not ui_state.engaged device_power = FeatureRow("device_power", "Device Power", "", available=power_available, actions=(("Reboot", True), ("Power Off", power_available and ui_state.is_offroad() and self.confirmed_offroad()))) master = replace(display_rows[0], page="display", reason=display_rows[0].reason or "Brightness and screen timing") state = replace(state, page="system", rows=(drive_row, device_power, master) + power_rows + maps.rows) return state def map_snapshot(self): if getattr(self, "map_source", None) is None: self.map_source = MapStatusSource() return self.map_source.snapshot() def model_snapshot(self): try: if self.model_source is None: self.model_source = ModelStatusSource(self.adapter.ui_state.params) return model_page(self.model_source.snapshot(), self.model_manager_snapshot()) except Exception: # Model status is optional diagnostics; UI remains usable without IPC. return model_page(project_status(None, None, None, None, time.monotonic_ns())) def _model_manager(self): if getattr(self, 'model_manager', None) is None: from openpilot.starpilot.models.manager import ModelManager self.model_manager = ModelManager(parked=self.confirmed_offroad, refresh_catalog=True) return self.model_manager def model_manager_snapshot(self): return self._model_manager().snapshot() def model_manager_action(self, action, payload): return self._model_manager().action(action, payload) def _models_ui(self, action: FeatureUiAction) -> None: if self._mode != ShellMode.SETTINGS or self.selected != Destination.DRIVING_MODEL: return state = self.model_snapshot() if action.kind == "back": self.selected = Destination.STAR self.model_scroll = 0 self.input.cancel() elif action.kind == "scroll": self.model_scroll = feature_scroll(self.model_scroll, action.direction, len(state.rows)) elif action.kind == "open" and action.row in state.rows and action.row.page in ("models:small", "models:big"): from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import DialogResult from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog profile = action.row.page.split(":", 1)[1] manager = self.model_manager_snapshot() choices = dict(model_profile_choices(manager, profile)) current = manager["activeSmallModel" if profile == "small" else "activeBigModel"] current_label = next((label for label, model_id in choices.items() if model_id == current), "") def confirmed(result): if result != DialogResult.CONFIRM or self._mode != ShellMode.SETTINGS or self.selected != Destination.DRIVING_MODEL: return fresh = self.model_manager_snapshot() if fresh["activeSmallModel" if profile == "small" else "activeBigModel"] != current: self._unavailable("saved model changed; reopen the selection") return request = model_profile_request(fresh, profile, choices.get(dialog.selection, "")) if request is not None: try: self.model_manager_action("active", request) except (OSError, ValueError, RuntimeError) as error: self._unavailable(str(error)) self._snapshot_cache = None dialog = MultiOptionDialog(f"Active {'Small' if profile == 'small' else 'Big'}", list(choices), current_label, callback=confirmed) gui_app.push_widget(dialog) self._snapshot_cache = None def power_request(self, request: FeatureSettingsRequest) -> bool: ok = self.power_owner.apply(request) self._snapshot_cache = None if not ok: self._unavailable("saved parked-power choice changed or settings closed") return ok def display_request(self, request: FeatureSettingsRequest) -> bool: ok = self.display_owner.apply(request) if ok: native_device.invalidate_display_preferences() self._snapshot_cache = None if not ok: self._unavailable("saved display choice changed or settings closed") return ok def _display_ui(self, action: FeatureUiAction) -> None: if self._mode != ShellMode.SETTINGS or self.selected != Destination.SYSTEM: return state = self.system_snapshot() if action.kind == "back": if state.editor: self.display_edit_key = None elif state.page == "display": self.display_page = "system" self.display_scroll = 0 else: self.selected = Destination.STAR self.display_scroll = 0 self.input.cancel() elif action.kind == "open" and not state.editor and action.row in state.rows: if action.row.key == DISPLAY_MASTER and action.row.page == "display": self.display_page = "display" self.display_scroll = 0 elif action.row.available and action.row.presets: self.display_edit_key = action.row.key else: return self.input.cancel() elif action.kind == "scroll" and not state.editor: self.display_scroll = feature_scroll(self.display_scroll, action.direction, len(state.rows)) elif action.kind in ("change", "action") and action.row is not None and action.row in state.rows: request = None if action.kind == "action" and action.row.key in ("drive_state", "device_power"): if not action.row.available or not 0 <= action.direction < len(action.row.actions) or not action.row.actions[action.direction][1]: return if action.row.key == "drive_state": self._drive_change(tuple(LABELS)[action.direction].value, action.row.source.decode() if action.row.source else None) elif self.system_power_action is not None: self.system_power_action(action.direction) elif state.editor: request = value_request(state, action) elif action.kind == "change" and not action.row.presets: request = power_row_change(action.row, action.direction) if action.row.key in POWER_KEYS else row_change(action.row, action.direction) else: return if request is not None: if request.key in POWER_KEYS: from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets import DialogResult epoch = getattr(self, "_power_request_epoch", 0) def confirmed(result: DialogResult) -> None: if (result == DialogResult.CONFIRM and self._mode == ShellMode.SETTINGS and self.selected == Destination.SYSTEM and getattr(self, "display_page", "system") == "system" and epoch == getattr(self, "_power_request_epoch", 0)): self.power_request(replace(request, confirmation=True)) gui_app.push_widget(ConfirmDialog(confirm_question(request), "Save", callback=confirmed)) else: if self.display_request(request) and request.key == DISPLAY_MASTER and request.value == "On" and state.page == "system": self.display_page = "display" self.display_scroll = 0 self.input.cancel() self._snapshot_cache = None def _drive_change(self, mode, revision, *, confirmation=False): if mode == "offroad" and not confirmation and (ui_state.started or self.drive_state.snapshot()["effective"] == "onroad"): from openpilot.system.ui.widgets import DialogResult destination = self.selected page = getattr(self, "display_page", "system") epoch = getattr(self, "_power_request_epoch", 0) pipeline = (bool(ui_state.started), ui_state.started_frame) def confirmed(result: DialogResult) -> None: if (result == DialogResult.CONFIRM and self._mode == ShellMode.SETTINGS and self.selected == destination and page == getattr(self, "display_page", "system") and epoch == getattr(self, "_power_request_epoch", 0) and pipeline == (bool(ui_state.started), ui_state.started_frame)): self._drive_change(mode, revision, confirmation=True) if self.profile == Profile.COMPACT: from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog gui_app.push_widget(BigConfirmationDialog("slide offroad", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 64), lambda: confirmed(DialogResult.CONFIRM), red=True)) else: from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog gui_app.push_widget(ConfirmDialog("Switch to Offroad and stop driving services?", "Force Offroad", callback=confirmed)) return try: self.drive_state.change(mode, revision, lambda: self._mode == ShellMode.SETTINGS) except DriveStateRejected as error: self.notice, self.notice_until = str(error), time.monotonic() + 3.0 except (OSError, RuntimeError): self.notice, self.notice_until = "Drive state unavailable", time.monotonic() + 3.0 self._snapshot_cache = None def _appearance_ui(self, action: FeatureUiAction) -> None: if self._mode != ShellMode.SETTINGS or self.selected != Destination.APPEARANCE: return state = self.appearance_snapshot() if action.kind == "back": if getattr(self, "appearance_edit_key", None) is not None: self.appearance_edit_key = None self._pip_request_epoch = getattr(self, "_pip_request_epoch", 0) + 1 elif getattr(self, "appearance_page", "appearance") == "pip": self.appearance_page = "appearance" self._pip_request_epoch = getattr(self, "_pip_request_epoch", 0) + 1 self.appearance_scroll = 0 else: self.selected = Destination.STAR self.appearance_scroll = 0 self.input.cancel() elif action.kind == "open" and not state.editor and action.row is not None and action.row in state.rows: if action.row.presets and action.row.available: self.appearance_edit_key = action.row.key elif action.row.page == "pip": self.appearance_page = "pip" self.appearance_scroll = 0 else: return self.input.cancel() elif action.kind == "scroll" and not state.editor: self.appearance_scroll = feature_scroll(self.appearance_scroll, action.direction, len(state.rows)) elif action.kind in ("change", "action") and action.row is not None and action.row in state.rows: request = value_request(state, action) if request is not None: self.appearance_request(request) elif action.kind == "reset" and action.row is not None and action.row in state.rows and \ (action.row.key == PIP_RESET or action.row.key.startswith(PIP_FORMAT_PREFIX)): row = action.row request = FeatureSettingsRequest(row.key, row.source, "confirm", confirmation=True, dependencies=row.dependencies) epoch = getattr(self, "_pip_request_epoch", 0) from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets import DialogResult def confirmed(result: DialogResult) -> None: if result == DialogResult.CONFIRM and self._mode == ShellMode.SETTINGS and \ self.selected == Destination.APPEARANCE and self.appearance_page == "pip" and \ epoch == getattr(self, "_pip_request_epoch", 0): self.appearance_request(request) size = row.key.removeprefix(PIP_FORMAT_PREFIX).replace("x", " × ") subject = ("Restore the default left and right camera crops?" if row.key == PIP_RESET else f"Use the {size} camera format with proportional starting crops? " + "Check alignment on the device.") question = (subject + " The saved preview switch will not change. If it is On, " + "the preview may resume on the next drive when a fresh camera frame is available.") gui_app.push_widget(ConfirmDialog(question, "Restore", callback=confirmed)) self._snapshot_cache = None def _sounds_ui(self, action: FeatureUiAction) -> None: if self._mode != ShellMode.SETTINGS or self.selected != Destination.SOUNDS: return state = self.sounds_snapshot() if action.kind == "back": if self.sounds_edit_key is not None: self.sounds_edit_key = None else: self.selected = Destination.STAR self.sounds_scroll = 0 self.input.cancel() elif action.kind == "open" and state.page == "sounds:overview" and action.row in state.rows and action.row.available: self.sounds_edit_key = action.row.key self.input.cancel() elif action.kind == "scroll" and self.sounds_edit_key is None: self.sounds_scroll = feature_scroll(self.sounds_scroll, action.direction, len(state.rows)) elif action.kind in ("change", "action") and state.page == "sounds" and action.row is not None and action.row in state.rows: request = value_request(state, action) if request is not None: self.sounds_request(request) self._snapshot_cache = None def _feature_ui(self, action: FeatureUiAction) -> None: if self._mode != ShellMode.SETTINGS or self.selected != Destination.DRIVING_CONTROLS: return state = self.feature_snapshot() if action.kind == "back": self._lane_change_request_epoch = getattr(self, "_lane_change_request_epoch", 0) + 1 if getattr(self, "feature_edit_key", None) is not None: self.feature_edit_key = None else: parent = feature_parent_page(self.feature_page) if parent is None or self.feature_page == self.feature_root_page: self.selected = Destination.STAR else: self.feature_page = parent self.feature_scroll = 0 self.input.cancel() elif action.kind == "scroll" and not state.editor: self.feature_scroll = feature_scroll(self.feature_scroll, action.direction, len(state.rows)) elif action.kind == "open" and not state.editor and action.row in state.rows and action.row.page: self._lane_change_request_epoch = getattr(self, "_lane_change_request_epoch", 0) + 1 if action.row.presets: if not action.row.available: return self.feature_edit_key = action.row.key elif action.row.page == SELECTION_PAGE: self._vehicle_selector().open(action.row.source) else: self.feature_page = action.row.page self.feature_scroll = 0 self.input.cancel() elif action.kind in ("change", "action") and action.row is not None and action.row in state.rows: request = value_request(state, action) if request is not None: if request.key in VEHICLE_BOOL_KEYS: self._confirm_vehicle_bool(request) elif request.key in ("CustomCruise", "CustomCruiseLong", "ToyotaRadarMode"): self._confirm_vehicle_bool(request, f"Set {action.row.label.lower()} to {request.value} {action.row.unit}".rstrip() + "?") elif request.key in LANE_CHANGE_KEYS: self._confirm_lane_change(request) else: self.feature_request(request) elif action.kind == "reset" and action.row is not None and action.row in state.rows: self._confirm_feature_reset(action.row) self._snapshot_cache = None def _confirm_feature_reset(self, row) -> None: from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets import DialogResult from openpilot.starpilot.ui.feature_settings_state import CURVE_CONFIRM_ACTIONS, CONDITIONAL_CONFIRM_ACTIONS, TORQUE_CONFIRM_ACTIONS, is_long_confirm_action from openpilot.starpilot.ui.long_profile_feature import long_confirm_question from openpilot.starpilot.ui.controller_feature import SETUP_ACTION, SETUP_QUESTION action_key = row.key if (row.key in TORQUE_CONFIRM_ACTIONS or row.key in ("slc_adopt", "slc_reset", LANE_CHANGE_RESET, SETUP_ACTION) or row.key in CURVE_CONFIRM_ACTIONS | CONDITIONAL_CONFIRM_ACTIONS or is_long_confirm_action(row.key)) else "reset_profiles" if action_key in CONDITIONAL_CONFIRM_ACTIONS: self._confirm_conditional(FeatureSettingsRequest(action_key, row.source, "confirm", confirmation=True, vehicle_fingerprint=row.vehicle_fingerprint, capability=row.capability, dependencies=row.dependencies)) return if action_key == LANE_CHANGE_RESET: self._confirm_lane_change(FeatureSettingsRequest(action_key, row.source, "confirm", vehicle_fingerprint=row.vehicle_fingerprint, capability=row.capability)) return question = {"torque_adopt": "Use the new torque editor for this vehicle model and configuration? Existing saved values will be kept but no longer used.", SETUP_ACTION: SETUP_QUESTION, "torque_reset": "Reset invalid torque profiles? Existing legacy values will be kept but remain inactive.", "torque_reset_profile": "Reset this vehicle model's custom torque profile to Vehicle/learned? Other models and legacy values remain saved.", "torque_gain_rebase": "Apply the saved steering response to the selected controller and current vehicle tune? " + "Friction and other models stay unchanged.", "torque_rebase": f"Reapply saved torque values ({row.value}) with the current vehicle tune? " + "Saved values must pass current limits.", "slc_adopt": "Keep these speed-limit offsets and speed ranges when units change? Legacy values remain saved but inactive.", "slc_reset": f"Reset speed-limit offsets to zero? {row.value}. " + "Saved SLC control may resume if its switch is On; the switch will not change.", "curve_reset": "Reset saved Curve learning? Your Curve Speed Controller On/Off choice will stay the same.", "reset_profiles": "Reset invalid saved Long Planner profiles to defaults?"}.get(action_key, long_confirm_question(row)) def confirmed(result: DialogResult) -> None: if result == DialogResult.CONFIRM: self.feature_request(FeatureSettingsRequest(action_key, row.source, "confirm", confirmation=True, related_source=row.related_source, vehicle_fingerprint=row.vehicle_fingerprint, capability=row.capability, dependencies=row.dependencies)) if action_key in ("torque_adopt", "slc_adopt"): button = "Continue" elif action_key in ("torque_rebase", "torque_gain_rebase"): button = "Review" elif action_key == SETUP_ACTION: button = "Prepare" else: button = "Restore" if action_key.startswith("long_repair:") else "Reset" gui_app.push_widget(ConfirmDialog(question, button, callback=confirmed)) def _confirm_lane_change(self, request: FeatureSettingsRequest) -> None: from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets import DialogResult epoch = self._lane_change_request_epoch question = lane_change_question(request) def confirmed(result: DialogResult) -> None: if (result == DialogResult.CONFIRM and self._mode == ShellMode.SETTINGS and self.selected == Destination.DRIVING_CONTROLS and self.feature_page == FeaturePage.LANE_CHANGE and epoch == self._lane_change_request_epoch): self.feature_request(replace(request, confirmation=True)) gui_app.push_widget(ConfirmDialog(question, "Save", callback=confirmed)) def _confirm_vehicle_bool(self, request: FeatureSettingsRequest, question: str | None = None) -> None: from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets import DialogResult page = self.feature_page epoch = self._lane_change_request_epoch question = question or vehicle_question(request) def confirmed(result: DialogResult) -> None: if result == DialogResult.CONFIRM and self._mode == ShellMode.SETTINGS and \ self.selected == Destination.DRIVING_CONTROLS and self.feature_page == page and \ epoch == self._lane_change_request_epoch: self.feature_request(replace(request, confirmation=True)) gui_app.push_widget(ConfirmDialog(question, "Save", callback=confirmed)) def _confirm_conditional(self, request: FeatureSettingsRequest) -> None: from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets import DialogResult page = self.feature_page epoch = self._lane_change_request_epoch question = conditional_question(request) def confirmed(result: DialogResult) -> None: if result == DialogResult.CONFIRM and self._mode == ShellMode.SETTINGS and \ self.selected == Destination.DRIVING_CONTROLS and self.feature_page == page and \ epoch == self._lane_change_request_epoch: self.feature_request(replace(request, confirmation=True)) gui_app.push_widget(ConfirmDialog(question, "Save", callback=confirmed)) def _live_slc_message(self, now_ns: int) -> Any | None: ui = self.adapter.ui_state if not ui.started: return None device = current_message(ui.sm, "deviceState", now_ns) pandas = current_message(ui.sm, "pandaStates", now_ns) if (device is None or not device.started or pandas is None or not any(p.ignitionLine or p.ignitionCan for p in pandas)): return None after = ui.started_frame car = current_message(ui.sm, "carState", now_ns, after_frame=after) control = current_message(ui.sm, "carControl", now_ns, after_frame=after) controls = current_message(ui.sm, "controlsState", now_ns, after_frame=after) selfdrive = current_message(ui.sm, "selfdriveState", now_ns, after_frame=after) cp = ui.CP if (car is None or not getattr(car, "canValid", False) or getattr(car, "canTimeout", True) or control is None or controls is None or selfdrive is None or cp is None): return None return current_message(ui.sm, "slcState", now_ns, after_frame=ui.started_frame) def snapshot(self, mode: ShellMode) -> ShellSnapshot: key = (self.adapter.ui_state.sm.frame, mode, self.selected, self.compact_y, self.compact_scroll_x, self.sidebar_expanded) if self._snapshot_cache is not None and self._snapshot_cache[0] == key: return self._snapshot_cache[1] snapshot = self.adapter.build(mode, self.selected, compact_y=self.compact_y, compact_scroll_x=self.compact_scroll_x, sidebar_expanded=self.sidebar_expanded, menu_only=self.profile == Profile.COMPACT and mode == ShellMode.SETTINGS, onroad_view_only=mode == ShellMode.ONROAD, stopped_timer_profile=str(self.profile)) if mode == ShellMode.ONROAD and self.profile == Profile.LARGE: width = getattr(self, '_onroad_page_width', 2160.0) - (SIDEBAR_WIDTH if snapshot.onroad.appearance.show_developer_sidebar else 0) fitted = fit_to_road(snapshot.onroad.customization, width) if snapshot.onroad.viewport_width != width or fitted is not snapshot.onroad.customization: snapshot = replace(snapshot, onroad=replace(snapshot.onroad, viewport_width=width, customization=fitted)) if mode == ShellMode.HOME: now = time.monotonic() previous = getattr(self, "_home_model", None) if previous is None or not 0 <= now - previous[0] < 1.0: try: if self.model_source is None: self.model_source = ModelStatusSource(self.adapter.ui_state.params) label = home_model_label(self.model_source.snapshot(), snapshot.home.commit if self.profile == Profile.COMPACT else "") except (OSError, RuntimeError, ValueError): label = "Driving model unavailable" self._home_model = (now, label) local_label = label if previous is None or not 0 <= now - previous[0] < 1.0 else previous[1] snapshot = replace(snapshot, home=replace(snapshot.home, model_label=snapshot.home.model_label or local_label)) preview_flags = getattr(self, "_visual_preview_flags", frozenset()) if mode == ShellMode.ONROAD and preview_flags: visual = preview_at(preview_flags, max(0, time.monotonic_ns() - self._visual_preview_start_ns) / 1e9) snapshot = replace(snapshot, onroad=replace(snapshot.onroad, visual_preview=visual)) if self.profile == Profile.COMPACT and mode == ShellMode.SETTINGS: drive = self.drive_state.snapshot() availability = tuple(replace(item, available=item.destination in (Destination.TOGGLES, Destination.DEVICE, Destination.SOFTWARE, Destination.DRIVING_MODEL, Destination.DRIVING_CONTROLS, Destination.APPEARANCE, Destination.GALAXY, Destination.NETWORK, Destination.BLUETOOTH, Destination.VEHICLE, Destination.DEVELOPER) or (item.destination == Destination.FORCE_DRIVE and drive["available"]) or snapshot.device.offroad and item.destination == Destination.PAIR) for item in snapshot.settings.availability) next_mode = DriveStateControl.next_mode(drive) label = ("unavailable" if not drive["available"] else {"offroad": "Force Off-road", "onroad": "Force On-road", "auto": "Return to Auto"}[next_mode.value]) availability = tuple(replace(item, request_value=next_mode.value, request_revision=drive["revision"], reason=None) if item.destination == Destination.FORCE_DRIVE else item for item in availability) snapshot = replace(snapshot, settings=replace(snapshot.settings, availability=availability, nav_bar_alpha=0, force_drive_label=label)) if self.profile == Profile.LARGE and mode == ShellMode.SETTINGS and self.selected == Destination.DRIVING_CONTROLS: feature = self.feature_snapshot() if not feature.editor: self.feature_scroll = feature_scroll(self.feature_scroll, 0, len(feature.rows)) feature = replace(feature, scroll=0 if feature.editor else self.feature_scroll, sidebar_expanded=self.sidebar_expanded, parent_title=feature.parent_title if feature.editor else "StarPilot" if self.feature_page == self.feature_root_page else feature_parent_title(self.feature_page)) snapshot = replace(snapshot, features=feature) if self.profile == Profile.LARGE and mode == ShellMode.SETTINGS and self.selected == Destination.SOUNDS: sounds = self.sounds_snapshot() if self.sounds_edit_key is None: self.sounds_scroll = feature_scroll(self.sounds_scroll, 0, len(sounds.rows)) sounds = replace(sounds, scroll=self.sounds_scroll if self.sounds_edit_key is None else 0, sidebar_expanded=self.sidebar_expanded) snapshot = replace(snapshot, sounds=sounds) if self.profile == Profile.LARGE and mode == ShellMode.SETTINGS and self.selected == Destination.APPEARANCE: appearance = self.appearance_snapshot() if not appearance.editor: self.appearance_scroll = feature_scroll(self.appearance_scroll, 0, len(appearance.rows)) appearance = replace(appearance, scroll=0 if appearance.editor else self.appearance_scroll, sidebar_expanded=self.sidebar_expanded, parent_title=appearance.parent_title if appearance.editor else "Appearance" if getattr(self, "appearance_page", "appearance") == "pip" else "StarPilot") snapshot = replace(snapshot, appearance=appearance) if self.profile == Profile.LARGE and mode == ShellMode.SETTINGS and self.selected == Destination.SYSTEM: display = self.system_snapshot() if not display.editor: self.display_scroll = feature_scroll(self.display_scroll, 0, len(display.rows)) display = replace(display, scroll=0 if display.editor else self.display_scroll, sidebar_expanded=self.sidebar_expanded) snapshot = replace(snapshot, display=display) if self.profile == Profile.LARGE and mode == ShellMode.SETTINGS and self.selected == Destination.DRIVING_MODEL: models = self.model_snapshot() self.model_scroll = feature_scroll(self.model_scroll, 0, len(models.rows)) models = replace(models, scroll=self.model_scroll, sidebar_expanded=self.sidebar_expanded) snapshot = replace(snapshot, models=models) self._snapshot_cache = (key, snapshot) return snapshot def render(self, mode: ShellMode, rect: rl.Rectangle, parent_clip: rl.Rectangle | None = None) -> None: self._poll_aol_save() if mode == ShellMode.ONROAD and self.profile == Profile.LARGE: if rect.width != self._onroad_page_width: self._onroad_page_width = rect.width self._snapshot_cache = None snapshot = self.snapshot(mode) self._mode = mode if mode == ShellMode.ONROAD: snapshot = replace(snapshot, onroad=self.input.onroad.present(snapshot.onroad)) if mode == ShellMode.ONROAD and self.slc_actions is not None: observation = snapshot.onroad.speed_limit snapshot = replace(snapshot, onroad=replace(snapshot.onroad, speed_limit=replace( observation, action_feedback=self.slc_actions.feedback(observation)))) elif mode != ShellMode.ONROAD and self.slc_actions is not None: self.slc_actions.clear_feedback() rendered = getattr(self, "_rendered_settings", None) if mode == ShellMode.SETTINGS and self.profile == Profile.LARGE and (rendered is None or rendered[1].selected != snapshot.selected): if pane := _FEATURE_SETTINGS_PANES.get(snapshot.selected): getattr(self.view, pane[0]).reset() self._rendered_settings = (time.monotonic_ns(), snapshot) if mode == ShellMode.SETTINGS else None self._rendered_settings_pipeline = (bool(ui_state.started), ui_state.started_frame) if mode == ShellMode.SETTINGS else None if mode == ShellMode.ONROAD: self._update_favorites(snapshot.onroad, time.monotonic()) self._poll_wheel(time.monotonic_ns()) else: self.favorites.cancel() if mode != ShellMode.ONROAD or not snapshot.onroad.camera_available: self.pip_warning.close() self._pip_signals_cache = None self._pip_saved = None self._vasm_saved = None self._pip_read_ns = None if renderer := getattr(self, "pip_renderer", None): renderer.deactivate() else: self._pip_signals_cache = self._pip_signals() snapshot = replace(snapshot, onroad=visual_border_state(snapshot.onroad, self._pip_signals_cache)) # Scroller and NavWidget place compact pages at changing screen positions. with placed_at(rect, parent_clip): external = (self.profile == Profile.LARGE and mode == ShellMode.SETTINGS and snapshot.selected in NATIVE_SETTINGS_PANELS and self.settings_layer is not None) pane = _FEATURE_SETTINGS_PANES.get(snapshot.selected) if pane and self.profile == Profile.LARGE: getattr(self.view, pane[0]).pressed_action = getattr(self.input, pane[2]).pressed_action drag_x = getattr(self.input, pane[2]).drag_x if pane else 0.0 self.view.render(replace(snapshot, selected=Destination.NETWORK) if external else snapshot, drag_x=drag_x) self._pip_signals_cache = None if mode == ShellMode.ONROAD: self.favorites.render(time.monotonic()) network = (mode == ShellMode.SETTINGS and self.profile == Profile.LARGE and snapshot.selected == Destination.NETWORK and snapshot.settings.destination(Destination.NETWORK).available and self.network_layer is not None) if network: rail_width = 500 if snapshot.settings.sidebar_expanded else 0 content = rl.Rectangle(rect.x + rail_width + 50, rect.y + 25, rect.width - rail_width - 100, rect.height - 50) if not self.network_layer(content) and self.selected == Destination.NETWORK: self.input.cancel() self.selected = Destination.STAR self._snapshot_cache = None if self._on_destination_change is not None: self._on_destination_change(Destination.STAR) self._unavailable("network panel is unavailable") if external: rail_width = 500 if snapshot.settings.sidebar_expanded else 0 content = rl.Rectangle(rect.x + rail_width + 50, rect.y + 25, rect.width - rail_width - 100, rect.height - 50) self.settings_layer(snapshot.selected, content) if external or network or self.notice: with placed_at(rect, parent_clip): if external or network: self.view.settings.render_rail(snapshot.settings, selected=snapshot.selected) if self.notice and time.monotonic() < self.notice_until: rl.draw_rectangle(0, self.profile.size[1] - 42, self.profile.size[0], 42, rl.Color(0, 0, 0, 220)) self.fonts.draw(self.notice[:65], FontRole.MEDIUM, 23 if self.profile == Profile.COMPACT else 27, 12, self.profile.size[1] - 37) def _unavailable(self, label: str) -> None: self.notice = f"Unavailable: {label}" self.notice_until = time.monotonic() + 3.0 def _emit(self, request: ShellRequest) -> None: self._request_emitted = True previous_destination = self.selected action = request.action if request.source == "onroad" and getattr(action, "kind", None) == "set_experimental": binding = self._native_favorite_actions().get(EXPERIMENTAL) if not (binding is not None and binding.available and binding.invoke is not None and action.action_token == binding.token and binding.invoke()): self._unavailable("experimental shortcut in this context") return if request.source == "home": if action.kind == HomeActionKind.OPEN_SETTINGS and self._on_settings is not None: self.selected = Destination.STAR self._on_settings() elif action.kind == HomeActionKind.OPEN_TOGGLES and self.profile == Profile.LARGE: self.selected = Destination.TOGGLES self._on_settings() elif (action.kind == HomeActionKind.OPEN_PAIRING and self.profile == Profile.LARGE and self._on_pairing is not None and not self.snapshot(ShellMode.HOME).home.paired and self.confirmed_offroad()): self._on_pairing() elif action.kind == HomeActionKind.OPEN_PAIRING: self._unavailable("device pairing") else: self._unavailable("experimental mode owner") elif request.source == "settings": if action.kind == SettingsActionKind.CLOSE: if self.selected == Destination.DRIVING_CONTROLS and getattr(self, "feature_edit_key", None) is not None: self._feature_ui(FeatureUiAction("back")) elif self.selected == Destination.APPEARANCE and getattr(self, "appearance_edit_key", None) is not None: self._appearance_ui(FeatureUiAction("back")) elif self.selected == Destination.SYSTEM and (getattr(self, "display_edit_key", None) is not None or getattr(self, "display_page", "system") == "display"): self._display_ui(FeatureUiAction("back")) elif self.selected != Destination.STAR: self.selected = Destination.STAR elif self._on_home is not None: self._on_home() elif action.kind == SettingsActionKind.TOGGLE_SIDEBAR: self.sidebar_expanded = not self.sidebar_expanded elif action.destination is not None: destination = action.destination.destination if destination == Destination.FORCE_DRIVE and action.destination.available: self._drive_change(action.destination.request_value, action.destination.request_revision) elif destination == Destination.STAR: self.selected = Destination.STAR elif action.destination.available and self.profile == Profile.LARGE: self.selected = destination if destination in (Destination.DRIVING_CONTROLS, Destination.VEHICLE): self.selected = Destination.DRIVING_CONTROLS self.feature_page = self.feature_root_page = FeaturePage.VEHICLE if destination == Destination.VEHICLE else FeaturePage.HUB self.feature_scroll = 0 self.feature_edit_key = None elif destination == Destination.SOUNDS: self.sounds_scroll = 0 self.sounds_edit_key = None elif destination == Destination.APPEARANCE: self.appearance_scroll = 0 self.appearance_page = "appearance" self.appearance_edit_key = None elif destination == Destination.SYSTEM: self.display_scroll = 0 self.display_page = "system" self.display_edit_key = None elif destination == Destination.DRIVING_MODEL: self.model_scroll = 0 elif action.destination.available and self._on_compact_destination is not None: self._on_compact_destination(destination) else: self._unavailable(destination.value) elif (request.source in ("toggles", "device", "software") and self._mode == ShellMode.SETTINGS and self.profile == Profile.LARGE and self._request_owner is not None and self._request_owner(action)): self._snapshot_cache = None # the owner acknowledged; read its state on the next frame elif request.source in ("features", "sounds", "appearance", "display", "models") and isinstance(action, FeatureUiAction) and action.kind == "details": self._feature_details(request.source, action) elif request.source == "features" and isinstance(action, FeatureUiAction): self._feature_ui(action) elif request.source == "sounds" and isinstance(action, FeatureUiAction): self._sounds_ui(action) elif request.source == "appearance" and isinstance(action, FeatureUiAction): self._appearance_ui(action) elif request.source == "display" and isinstance(action, FeatureUiAction): self._display_ui(action) elif request.source == "models" and isinstance(action, FeatureUiAction): self._models_ui(action) elif request.source == "slc" and isinstance(action, SlcUiRequest): if self.slc_actions is None or not self.slc_actions.dispatch(action): self._unavailable("speed limit action") else: self._unavailable(request.source + " request") if self.selected != previous_destination: self._power_request_epoch = getattr(self, "_power_request_epoch", 0) + 1 self._pip_request_epoch = getattr(self, "_pip_request_epoch", 0) + 1 self._lane_change_request_epoch = getattr(self, "_lane_change_request_epoch", 0) + 1 if self._on_destination_change is not None: self.input.cancel() self._on_destination_change(self.selected) def _feature_details(self, source: str, action: FeatureUiAction) -> None: destination, snapshot = {"features": (Destination.DRIVING_CONTROLS, self.feature_snapshot), "sounds": (Destination.SOUNDS, self.sounds_snapshot), "appearance": (Destination.APPEARANCE, self.appearance_snapshot), "display": (Destination.SYSTEM, self.system_snapshot), "models": (Destination.DRIVING_MODEL, self.model_snapshot)}[source] if self._mode != ShellMode.SETTINGS or self.selected != destination: return state = snapshot() row = action.row if row is not None: if row not in state.rows: return value = row.value + (" " + row.unit if row.unit and row.value != "Auto" else "") parts = (row.label, value, row.reason, f"Set {row.repair_value}" if row.repair_value else "") else: parts = (state.title, state.subtitle) from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog dialog = ConfirmDialog("".join(f"
{escape(part)}
" for part in parts if part), "Close", cancel_text="", rich=True) # Native scrolling preserves full explanations; decode literals after the HTML parser. for element in dialog._html_renderer.elements: element.content = unescape(element.content) element.font_size = 62 # Match size-50 bitmap body text at the large profile scale. self.input.cancel() gui_app.push_widget(dialog) def _input_snapshot(self, mode: ShellMode) -> ShellSnapshot: snapshot = self.snapshot(mode) if mode == ShellMode.ONROAD and self.profile == Profile.LARGE: binding = self._native_favorite_actions().get(EXPERIMENTAL) snapshot = replace(snapshot, onroad=replace( snapshot.onroad, experimental_available=bool(binding is not None and binding.available), experimental_action_token=binding.token if binding is not None else "")) return snapshot def _settings_press_snapshot(self) -> ShellSnapshot: rendered = getattr(self, "_rendered_settings", None) now_ns = time.monotonic_ns() if (rendered is not None and 0 <= now_ns - rendered[0] <= 250_000_000 and rendered[1].selected == self.selected and (not rendered[1].device.offroad or self.confirmed_offroad()) and getattr(self, "_rendered_settings_pipeline", None) == (bool(ui_state.started), ui_state.started_frame)): snapshot = rendered[1] else: snapshot = self.snapshot(ShellMode.SETTINGS) self._settings_touch = snapshot self._settings_pipeline = (bool(ui_state.started), ui_state.started_frame) return snapshot def _settings_gesture_snapshot(self) -> ShellSnapshot | None: snapshot = getattr(self, "_settings_touch", None) if snapshot is None or snapshot.selected != self.selected: self.cancel() return None if (pane := _FEATURE_SETTINGS_PANES.get(self.selected)) and self.profile == Profile.LARGE: state = getattr(snapshot, pane[0]) edit_key = getattr(self, pane[1] + "_edit_key", None) scroll = 0 if edit_key is not None else getattr(self, pane[1] + "_scroll") if (state.scroll != scroll or state.editor != (edit_key is not None) or (state.editor and state.rows[0].key != edit_key) or state.page != getattr(self, pane[1] + "_page", state.page) or state.sidebar_expanded != self.sidebar_expanded): self.cancel() return None if snapshot.device.offroad and not self.confirmed_offroad(): self.cancel() return None if getattr(self, "_settings_pipeline", None) != (bool(ui_state.started), ui_state.started_frame): self.cancel() return None return snapshot def press(self, mode: ShellMode, x: float, y: float) -> None: self._request_emitted = False self._mode = mode if mode == ShellMode.SETTINGS: snapshot = self._settings_press_snapshot() self.input.press(x, y, time.monotonic(), snapshot) if (pane := _FEATURE_SETTINGS_PANES.get(snapshot.selected)) and self.profile == Profile.LARGE: state = getattr(snapshot, pane[0]) if getattr(self.view, pane[0]).reset(state) and FeatureInput._in_body(x, y, state): getattr(self.input, pane[2]).cancel_tap() return now, snapshot = time.monotonic(), self.snapshot(mode) self._favorite_claimed = False self._navigation_claimed = False self._onroad_claimed = False if mode == ShellMode.ONROAD: self._update_favorites(snapshot.onroad, now) if self.favorites.is_open: self._favorite_claimed = self.favorites.press(x, y, now) self.input.cancel() return self.input.press(x, y, now, self._input_snapshot(mode)) self._onroad_claimed = mode == ShellMode.ONROAD and self.input.onroad.claimed if mode == ShellMode.ONROAD and not self.input.onroad.claimed: if self.view.onroad.navigation.press(x, y, snapshot.onroad): self._navigation_claimed = True self.input.cancel() return self._favorite_claimed = self.favorites.press(x, y, now) if self._favorite_claimed: self.input.cancel() def move(self, mode: ShellMode, x: float, y: float, *, timestamp: float | None = None) -> None: self._mode = mode if mode == ShellMode.SETTINGS: if snapshot := self._settings_gesture_snapshot(): self.input.move(x, y, timestamp, snapshot) return if getattr(self, "_navigation_claimed", False): self.view.onroad.navigation.move(x, y, self.snapshot(mode).onroad) return if self._favorite_claimed: self._update_favorites(self.snapshot(mode).onroad, time.monotonic()) self.favorites.move(x, y) return self.input.move(x, y, time.monotonic(), self._input_snapshot(mode)) def release(self, mode: ShellMode, x: float, y: float, *, timestamp: float | None = None) -> bool: self._mode = mode if mode == ShellMode.SETTINGS: snapshot = self._settings_gesture_snapshot() self._settings_touch = None if snapshot is not None: self.input.release(x, y, timestamp, snapshot) return self._request_emitted if getattr(self, "_navigation_claimed", False): self._navigation_claimed = False self.view.onroad.navigation.release(x, y, self.snapshot(mode).onroad) return True if self._favorite_claimed: self._favorite_claimed = False now = time.monotonic() self._update_favorites(self.snapshot(mode).onroad, now) self.favorites.release(x, y, now) return True self.input.release(x, y, time.monotonic(), self._input_snapshot(mode)) return self._request_emitted or getattr(self, '_onroad_claimed', False) def cancel(self) -> None: if pending := getattr(self, "_aol_save", None): pending.cancel.set() self._settings_touch = None if (pane := _FEATURE_SETTINGS_PANES.get(self.selected)) and self.profile == Profile.LARGE: getattr(self.view, pane[0]).reset() self._power_request_epoch = getattr(self, "_power_request_epoch", 0) + 1 self._pip_request_epoch = getattr(self, "_pip_request_epoch", 0) + 1 self._lane_change_request_epoch = getattr(self, "_lane_change_request_epoch", 0) + 1 self.input.cancel() self.favorites.cancel() self.view.onroad.navigation.cancel() self._navigation_claimed = False self._favorite_claimed = False def close(self) -> None: if layer := getattr(self, 'onroad_jetlink', None): layer.close() self.onroad_jetlink = None if layer := getattr(self, 'onroad_map', None): layer.close() self.onroad_map = None if receiver := getattr(self, '_projection_bookmarks', None): receiver.close() self._projection_bookmarks = None if receiver := getattr(self, '_projection_slc', None): receiver.close() self._projection_slc = None self._aol_save_closed = True if pending := getattr(self, "_aol_save", None): pending.cancel.set() self.drive_state.physical.close() self.slc_actions = None self.galaxy_flow.close() if getattr(self, 'model_manager', None) is not None: self.model_manager.close() if self.bluetooth_source is not None: self.bluetooth_source.close() self.pip_warning.close() self.pip_renderer.close() if self.model_source is not None: self.model_source.close() self.model_source = None if self.map_source is not None: self.map_source.close() self.map_source = None self.view.close() self.fonts.close() class StarShellPage(Widget): def __init__(self, session: StarShellSession, mode: ShellMode, *, on_background_tap: Any = None): super().__init__() self.session, self.mode = session, mode self.on_background_tap = on_background_tap self._press_pos: tuple[float, float] | None = None self._dragged = False self._touch_time: float | None = None def _render(self, rect: rl.Rectangle) -> None: self.session.render(self.mode, rect, self._parent_rect) if self.mode == ShellMode.ONROAD and self.session.profile == Profile.COMPACT: bookmark = self.session.camera_owner._bookmark_icon bookmark.set_touch_valid_callback(lambda: self._touch_valid() and abs(self.rect.x) < 1) bookmark.set_parent_rect(self._parent_rect or self.rect) bookmark.render(rect) def _handle_mouse_press(self, mouse_pos: MousePos) -> None: self._press_pos = (mouse_pos.x, mouse_pos.y) self._dragged = False self._touch_time = None self.session.press(self.mode, mouse_pos.x - self.rect.x, mouse_pos.y - self.rect.y) def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: self._touch_time = mouse_event.t if mouse_event.left_down and (not mouse_event.left_pressed or self.mode == ShellMode.SETTINGS): self._track_background_drag(mouse_event.pos) self.session.move(self.mode, mouse_event.pos.x - self.rect.x, mouse_event.pos.y - self.rect.y, timestamp=mouse_event.t) elif mouse_event.left_released and not rl.check_collision_point_rec(mouse_event.pos, self.rect): self.session.cancel() def _handle_mouse_release(self, mouse_pos: MousePos) -> None: self._track_background_drag(mouse_pos) handled = self.session.release(self.mode, mouse_pos.x - self.rect.x, mouse_pos.y - self.rect.y, timestamp=self._touch_time) bookmark_handled = (self.mode == ShellMode.ONROAD and self.session.profile == Profile.COMPACT and self.session.camera_owner._bookmark_icon.interacting()) if not handled and not self._dragged and not bookmark_handled and self.on_background_tap is not None: self.on_background_tap() self._press_pos = None self._dragged = False def _track_background_drag(self, pos: MousePos) -> None: tolerance = 36 if self.session.profile == Profile.LARGE else 5 if self._press_pos is not None and math.hypot(pos.x - self._press_pos[0], pos.y - self._press_pos[1]) > tolerance: self._dragged = True def hide_event(self) -> None: self.session.cancel() if self.mode == ShellMode.ONROAD and self.session.profile == Profile.COMPACT: self.session.camera_owner._bookmark_icon.hide_event() self._press_pos = None self._dragged = False super().hide_event() class StarMainLayout(MainLayout): """Large existing app transitions/onboarding with full-canvas StarPilot views.""" def __init__(self): validate_runtime_fonts(Profile.LARGE) validate_runtime_assets() super().__init__() self._native_onroad = self._layouts[MainState.ONROAD] from openpilot.selfdrive.ui.layouts.settings.settings import PanelType native_network = self._layouts[MainState.SETTINGS]._panels[PanelType.NETWORK].instance self._network_bridge = NetworkPanelBridge(native_network, self._network_authority) self._large_panels = { destination: self._layouts[MainState.SETTINGS]._panels[panel].instance for destination, panel in ((Destination.DEVICE, PanelType.DEVICE), (Destination.SOFTWARE, PanelType.SOFTWARE), (Destination.TOGGLES, PanelType.TOGGLES), (Destination.DEVELOPER, PanelType.DEVELOPER)) } self._large_destination = None try: self.star = StarShellSession(Profile.LARGE, self._native_onroad, network_layer=self._network_bridge.render, settings_layer=self._render_large_panel) except Exception: self._native_onroad.close() raise self.page = StarShellPage(self.star, ShellMode.HOME, on_background_tap=self._on_background_tap) device_scroller = self._large_panels[Destination.DEVICE]._scroller device_scroller._items.remove(self._large_panels[Destination.DEVICE]._power_off_btn) self.star.system_power_action = self._system_power_action device_scroller.add_widget( button_item("Galaxy", "OPEN", callback=self.star.galaxy_flow.open_large)) device_scroller._items.insert(0, device_scroller._items.pop()) from openpilot.starpilot.ui.replay_settings import open_replay device_scroller.add_widget(button_item("Replay", "OPEN", callback=lambda: open_replay(self.star))) device_scroller._items.insert(1, device_scroller._items.pop()) toggle_items = self._large_panels[Destination.TOGGLES]._scroller._items toggle_items.insert(2, text_item("Safe Mode", "N/A")) toggle_items.insert(7, text_item("Right Hand Driving", "N/A")) from openpilot.common.hardware import HARDWARE from openpilot.starpilot.ui.starpilot_settings_adapter_large import StarPilotSettingsAdapterLarge for destination, owner in self._large_panels.items(): if destination == Destination.DEVELOPER and HARDWARE.get_device_type() in ('mici', 'tici', 'tizi'): from openpilot.starpilot.ui.spacepilot.large import SpacePilotDeveloperAdapter self._large_panels[destination] = SpacePilotDeveloperAdapter(owner, self.star.fonts, self._settings_back, self._exit_spacepilot) else: self._large_panels[destination] = StarPilotSettingsAdapterLarge(owner, self.star.fonts, destination.value.title(), self._settings_back) self._network_bridge.presentation = StarPilotSettingsAdapterLarge(native_network, self.star.fonts, "Network", self._settings_back) self.star.set_navigation(on_settings=lambda: self._set_current_layout(MainState.SETTINGS), on_home=self._set_mode_for_state, on_pairing=self._show_pairing, on_destination_change=self._network_destination) self.star.set_request_owner(self._deliver_settings_request) self.star._native_favorite_actions = self._native_favorites self.star._publish_bookmark = self._on_bookmark_clicked def _exit_spacepilot(self) -> None: gui_app.pop_widgets_to(self, instant=True) self._set_mode_for_state() def _native_favorites(self): from openpilot.selfdrive.ui.layouts.settings.settings import PanelType return self.star.native_favorite_actions( lambda: (self._on_bookmark_clicked() or True), lambda index: self._panel_owner(PanelType.TOGGLES).request_personality(index), self._native_onroad._hud_renderer._exp_button.request_toggle) def _confirmed_offroad(self) -> bool: # Cached touch snapshots need fresh transport and native gates before effects. return bool(ui_state.is_offroad() and self.star.adapter.confirmed_offroad()) def _arabesque_allowed(self) -> bool: return self._confirmed_offroad() def _system_power_action(self, index: int) -> None: from openpilot.selfdrive.ui.layouts.settings.settings import PanelType def allowed() -> bool: return (self._current_mode == MainState.SETTINGS and self.star._mode == ShellMode.SETTINGS and self.star.selected == Destination.SYSTEM and getattr(self.star, "display_page", "system") == "system" and (index == 0 or self._confirmed_offroad())) if index in (0, 1) and allowed(): owner = self._panel_owner(PanelType.DEVICE) (owner._reboot_prompt, owner._power_off_prompt)[index](allowed) def _show_pairing(self) -> None: if self._confirmed_offroad() and not ui_state.prime_state.is_paired(): from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog gui_app.push_widget(PairingDialog()) def _network_authority(self) -> bool: return self._current_mode == MainState.SETTINGS and self.star.selected == Destination.NETWORK def _render_large_panel(self, destination: Destination, rect) -> None: self._network_destination(destination) panel = self._large_panels.get(destination) if panel is not None and self._large_destination == destination: panel.render(rect) def _settings_back(self) -> None: self.star._emit(ShellRequest("settings", SettingsAction(SettingsActionKind.CLOSE))) def _leave_large_panel(self) -> None: if self._large_destination is not None: self._large_panels[self._large_destination].hide_event() self._large_destination = None def _network_destination(self, destination: Destination) -> None: if destination != self._large_destination: self._leave_large_panel() if destination == Destination.BLUETOOTH: if destination not in self._large_panels: from openpilot.starpilot.ui.bluetooth_large import BluetoothLarge from openpilot.starpilot.ui.starpilot_settings_adapter_large import StarPilotSettingsAdapterLarge self._large_panels[destination] = StarPilotSettingsAdapterLarge( BluetoothLarge(self.star.connectivity_allowed), self.star.fonts, "Bluetooth", self._settings_back) if destination in self._large_panels: self._large_destination = destination self._large_panels[destination].show_event() if destination == Destination.NETWORK: if not self._network_bridge.enter(): self.star.selected = Destination.STAR self.star._unavailable("network panel is unavailable") else: self._network_bridge.leave() def _panel_owner(self, panel: Any) -> Any: return self._layouts[MainState.SETTINGS]._panels[panel].instance def _deliver_settings_request(self, request: ToggleRequest | DeviceAction | SoftwareAction) -> bool: if isinstance(request, ToggleRequest): return self.star.selected == Destination.TOGGLES and self._deliver_toggles(request) if isinstance(request, DeviceAction) and request.request == DeviceRequest.OPEN_GALAXY: return self.star.selected == Destination.DEVICE and self._deliver_device(request) if not self._confirmed_offroad(): return False if isinstance(request, DeviceAction): return self.star.selected == Destination.DEVICE and self._deliver_device(request) if isinstance(request, SoftwareAction): return self.star.selected == Destination.SOFTWARE and self._deliver_software(request) return False def _deliver_toggles(self, request: ToggleRequest) -> bool: from openpilot.selfdrive.ui.layouts.settings.settings import PanelType owner = self._panel_owner(PanelType.TOGGLES) if request.personality is not None: return owner.request_personality(list(Personality).index(request.personality)) if request.key is None or request.value is None: return False param = {ToggleKey.ENABLED: "OpenpilotEnabledToggle", ToggleKey.EXPERIMENTAL: "ExperimentalMode", ToggleKey.DISENGAGE_ACCELERATOR: "DisengageOnAccelerator", ToggleKey.LANE_DEPARTURE: "IsLdwEnabled", ToggleKey.ALWAYS_ON_DM: "AlwaysOnDM", ToggleKey.RECORD_FRONT: "RecordFront", ToggleKey.RECORD_AUDIO: "RecordAudio", ToggleKey.METRIC: "IsMetric"}.get(request.key) return bool(param is not None and owner.request_toggle(param, request.value)) def _deliver_device(self, request: DeviceAction) -> bool: from openpilot.selfdrive.ui.layouts.settings.settings import PanelType fresh = self.star.adapter.build(ShellMode.SETTINGS, Destination.DEVICE, now_ns=time.monotonic_ns()).device if request.request not in fresh.available_actions: return False owner = self._panel_owner(PanelType.DEVICE) if request.request == DeviceRequest.PREVIEW_DRIVER_CAMERA: from openpilot.selfdrive.ui.onroad.cabin_camera_dialog import CabinCameraDialog gui_app.push_widget(CabinCameraDialog()) elif request.request == DeviceRequest.RESET_CALIBRATION: owner._reset_calibration_prompt(self._confirmed_offroad) elif request.request == DeviceRequest.OPEN_GALAXY: self.star.galaxy_flow.open_large() else: return False return True def _deliver_software(self, request: SoftwareAction) -> bool: return deliver_software(self, request, shell_mode=ShellMode, destination=Destination, clock=time, software_request=SoftwareRequest, download_label=DownloadLabel) def _render_main_content(self) -> None: if self._current_mode == MainState.ARABESQUE or self._current_mode == MainState.HOME and ui_state.is_body: super()._render_main_content() return rect = self._rect if self._current_mode == MainState.ONROAD: self._update_layout_rects() if self._sidebar.is_visible: self._sidebar.render(self._sidebar_rect) if self._current_mode != MainState.ONROAD or not self._sidebar.is_visible: return rect = self._content_rect mode = {MainState.HOME: ShellMode.HOME, MainState.SETTINGS: ShellMode.SETTINGS, MainState.ONROAD: ShellMode.ONROAD}[self._current_mode] self.page.mode = mode self.page.render(rect) def _on_background_tap(self) -> None: if self._current_mode == MainState.ONROAD: self._on_onroad_clicked() def _set_current_layout(self, layout: MainState) -> None: if layout != MainState.SETTINGS and hasattr(self, "_network_bridge"): self._leave_large_panel() self._network_bridge.leave() if hasattr(self, "star") and self.star.selected == Destination.NETWORK: self.star.selected = Destination.STAR self.star.cancel() self.star._snapshot_cache = None changed = layout != self._current_mode if changed and hasattr(self, "page"): self.page.hide_event() super()._set_current_layout(layout) if changed and hasattr(self, "page") and layout != MainState.ARABESQUE: self.page.show_event() def open_settings(self, panel_type: Any) -> None: from openpilot.selfdrive.ui.layouts.settings.settings import PanelType # Separate SettingsLayout from the shell's NetworkUI to avoid duplicate scans. native_panel = PanelType.DEVICE if panel_type == PanelType.NETWORK else panel_type super().open_settings(native_panel) if hasattr(self, "star"): if panel_type == PanelType.NETWORK: self.star.selected = Destination.NETWORK self._network_destination(Destination.NETWORK) elif self.star.selected == Destination.NETWORK: self._network_bridge.leave() self.star.selected = Destination.STAR self.star.cancel() self.star._snapshot_cache = None def _on_body_changed(self) -> None: if hasattr(self, "page"): self.page.hide_event() super()._on_body_changed() def close(self) -> None: super().close() self._leave_large_panel() self._network_bridge.leave() self.star.close() self._native_onroad.close() class StarMiciMainLayout(MiciMainLayout): """Compact existing scroller/nav stack with StarPilot content pages.""" def __init__(self): validate_runtime_fonts(Profile.COMPACT) validate_runtime_assets() super().__init__() self._native_onroad = self._car_onroad_layout self._compact_panels: dict[Destination, Widget] = {} try: self.star = StarShellSession(Profile.COMPACT, self._native_onroad) except Exception: self._native_onroad.close() raise self._home_layout = StarShellPage(self.star, ShellMode.HOME) self._car_onroad_layout = StarShellPage(self.star, ShellMode.ONROAD, on_background_tap=lambda: self._scroll_to(self._home_layout)) self._settings_layout = StarCompactSettings(self.star) self._scroller.items[1] = self._home_layout self._scroller.items[2] = self._car_onroad_layout for page in (self._home_layout, self._car_onroad_layout): page.set_touch_valid_callback(lambda: self._scroller.scroll_panel.is_touch_valid() and self.enabled) for page in (self._home_layout, self._car_onroad_layout, self._settings_layout): page.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) self.star.set_navigation(on_settings=lambda: gui_app.push_widget(self._settings_layout), on_home=lambda: self._settings_layout.dismiss(), on_compact_destination=self._open_compact_destination) self._scroller.set_scrolling_enabled(lambda: not self._native_onroad.is_swiping_left()) self._on_body_changed() self.star._native_favorite_actions = self._native_favorites self.star._publish_bookmark = self._on_bookmark_clicked def _render(self, _) -> None: rect = _ page = self._car_onroad_layout bookmark = self._native_onroad._bookmark_icon bookmark.set_rect(page.rect) bookmark.set_parent_rect(self.rect) if not (self.enabled and page.enabled and page.is_visible): bookmark.cancel_gesture() bookmark.process_gesture(can_start=(self.enabled and page.enabled and page.is_visible and page._touch_valid() and not self._scroller.is_auto_scrolling and abs(page.rect.x - self.rect.x) < 1)) self.star.camera_paint = not self._settings_layout.covers_camera(rect) try: super()._render(rect) finally: self.star.camera_paint = True def _native_favorites(self): def panel(): from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici if Destination.TOGGLES not in self._compact_panels: self._compact_panels[Destination.TOGGLES] = TogglesLayoutMici() return self._compact_panels[Destination.TOGGLES] return self.star.native_favorite_actions(lambda: (self._on_bookmark_clicked() or True), lambda index: panel().request_personality(index), lambda: panel().request_experimental()) def _handle_transitions(self): from openpilot.starpilot.ui.spacepilot.native import SpacePilotPage, Stage, live_witness page = gui_app.get_active_widget() if (isinstance(page, SpacePilotPage) and not page.closed and page.session.stage != Stage.CLOSED and live_witness() is not None): # Cancel only the cosmetic delayed startup redirect for a freshly parked # game. The base movement transition and the game's own gates still run. self._onroad_time_delay = None super()._handle_transitions() def _exit_spacepilot(self) -> None: # An unsafe game exit must reveal the existing road/alert presentation now, # including when engagement/alerts change while the car still reports Park. gui_app.pop_widgets_to(self, instant=True) self._scroller.scroll_to(self._onroad_layout.rect.x if ui_state.started else self._home_layout.rect.x, smooth=False) def _open_compact_destination(self, destination: Destination) -> None: if destination == Destination.BLUETOOTH: from openpilot.starpilot.ui.bluetooth_compact import BluetoothCompact panel = self._compact_panels.get(destination) if panel is None: panel = BluetoothCompact(self.star.connectivity_allowed) self._compact_panels[destination] = panel gui_app.push_widget(panel) return if destination == Destination.GALAXY: self.star.galaxy_flow.open_compact() return if destination == Destination.DRIVING_MODEL: from openpilot.starpilot.ui.models_compact import ModelsCompact ModelsCompact(self.star).open() return if destination == Destination.DRIVING_CONTROLS: from openpilot.starpilot.ui.feature_settings_compact import FeatureSettingsCompact FeatureSettingsCompact(self.star).open(FeaturePage.HUB) return if destination == Destination.APPEARANCE: from openpilot.starpilot.ui.appearance_compact import AppearanceCompact AppearanceCompact(self.star).open() return if destination == Destination.VEHICLE: from openpilot.starpilot.ui.vehicle_compact import VehicleCompact gui_app.push_widget(VehicleCompact(self.star)) return if destination == Destination.PAIR: self._open_compact_destination(Destination.DEVICE) self._compact_panels[Destination.DEVICE].scroll_to_pairing() return from openpilot.selfdrive.ui.mici.layouts.settings.device.device_layout import DeviceLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.network.network_layout import NetworkLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.software import SoftwareLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici panel_types = {Destination.TOGGLES: TogglesLayoutMici, Destination.NETWORK: NetworkLayoutMici, Destination.DEVICE: DeviceLayoutMici, Destination.SOFTWARE: SoftwareLayoutMici, Destination.DEVELOPER: DeveloperLayoutMici} panel_type = panel_types.get(destination) if panel_type is None: self.star._unavailable(destination.value) return panel = self._compact_panels.get(destination) if panel is None: panel = DeveloperLayoutMici(on_spacepilot_exit=self._exit_spacepilot) if destination == Destination.DEVELOPER else panel_type() if destination == Destination.DEVICE: from openpilot.starpilot.ui.display_compact import DisplayCompact from openpilot.starpilot.ui.replay_settings import replay_entry panel._scroller.add_widgets([DisplayCompact(self.star).entry_button(), replay_entry(self.star)]) self._compact_panels[destination] = panel gui_app.push_widget(panel) def close(self) -> None: self.star.close() self._native_onroad.close()