Files
StarPilot/openpilot/selfdrive/ui/mici/layouts/settings/device/device_layout.py
T
firestar5683 a82935f326 User interfaces and Android Auto
Integrate onroad views, configurable widgets, local settings, steering-profile contracts, Bluetooth and Android Auto with their device and runtime services.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: Shane Smiskol <shane@smiskol.com>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: stef <19478336+stefpi@users.noreply.github.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: pharmacomaniac <blittle65@gmail.com>

Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

254 lines
12 KiB
Python

import os
import pyray as rl
from collections.abc import Callable
from typing import Union
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.time_helpers import system_time_valid
from openpilot.selfdrive.ui.mici.layouts.settings.device.prime import PrimeScroller
from openpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller
from openpilot.selfdrive.ui.mici.widgets.info import InfoLayoutMici
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog
from openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog import CabinCameraDialog
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage
from openpilot.system.ui.lib.application import gui_app, MousePos
from openpilot.system.ui.lib.multilang import tr
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.starpilot.ui.slc_offset_feature import native_parked
from openpilot.starpilot.ui.calibration_reset import calibration_reset
from openpilot.system.ui.widgets.html_render import HtmlRenderer
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
class ReviewTermsPage(TermsPage, NavScroller):
"""TermsPage with NavWidget swipe-to-dismiss for reviewing in device settings."""
def __init__(self):
super().__init__(on_accept=self.dismiss, on_decline=self.dismiss)
self._terms_header.set_visible(False)
self._must_accept_card.set_visible(False)
self._accept_button.set_visible(False)
self._decline_button.set_visible(False)
class ReviewTrainingGuide(TrainingGuide):
def show_event(self):
super().show_event()
device.set_override_interactive_timeout(300)
def hide_event(self):
super().hide_event()
device.set_override_interactive_timeout(None)
ui_state.params.put_bool("IsDriverViewEnabled", False)
class MiciFccModal(NavRawScrollPanel):
def __init__(self, file_path: str | None = None, text: str | None = None):
super().__init__()
self._content = HtmlRenderer(file_path=file_path, text=text)
self._fcc_logo = gui_app.texture("icons_mici/settings/device/fcc_logo.png", 76, 64)
def _render(self, rect: rl.Rectangle):
content_height = self._content.get_total_height(int(rect.width))
content_height += self._fcc_logo.height + 20
scroll_content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
scroll_offset = round(self._scroll_panel.update(rect, scroll_content_rect.height))
fcc_pos = rl.Vector2(rect.x + 20, rect.y + 20 + scroll_offset)
scroll_content_rect.y += scroll_offset + self._fcc_logo.height + 20
self._content.render(scroll_content_rect)
rl.draw_texture_ex(self._fcc_logo, fcc_pos, 0.0, 1.0, rl.WHITE)
def _engaged_confirmation_click(callback: Callable, action_text: str, icon: rl.Texture, exit_on_confirm: bool = True, red: bool = False):
if not ui_state.engaged:
def confirm_callback():
# Check engaged again in case it changed while the dialog was open
# TODO: if true, we stay on the dialog if not exit_on_confirm until normal onroad timeout
if not ui_state.engaged:
callback()
gui_app.push_widget(BigConfirmationDialog(f"slide to\n{action_text.lower()}", icon, confirm_callback, exit_on_confirm=exit_on_confirm, red=red))
else:
gui_app.push_widget(BigDialog("", f"Disengage to {action_text}"))
class EngagedConfirmationCircleButton(BigCircleButton):
def __init__(self, title: str, icon: rl.Texture, callback: Callable[[], None], exit_on_confirm: bool = True,
red: bool = False, icon_offset: tuple[int, int] = (0, 0), *, description: str = ""):
super().__init__(icon, red, icon_offset, description=description, title=title)
self.set_click_callback(lambda: _engaged_confirmation_click(callback, title, icon, exit_on_confirm=exit_on_confirm, red=red))
class EngagedConfirmationButton(BigButton):
def __init__(self, text: str, action_text: str, icon: rl.Texture, callback: Callable[[], None],
exit_on_confirm: bool = True, red: bool = False, *, description: str = "",
description_icon: Union[rl.Texture, None] = None):
super().__init__(text, "", icon, description=description, description_icon=description_icon)
self.set_click_callback(lambda: _engaged_confirmation_click(callback, action_text, icon, exit_on_confirm=exit_on_confirm, red=red))
class DeviceInfoLayoutMici(InfoLayoutMici):
def __init__(self):
params = Params()
super().__init__("device ID", params.get("DongleId") or 'N/A', "serial", params.get("HardwareSerial") or 'N/A', width=380)
class PairBigButton(BigButton):
def __init__(self):
self._commacare_badge = gui_app.texture("icons_mici/settings/device/commacare.png", 27, 32)
self._show_commacare = False
self._comma_icon = gui_app.texture("icons_mici/settings/comma_icon.png", 33, 60)
self._provider_icons = {provider: gui_app.texture(f"icons_mici/settings/device/paired_{provider}.png", 64, 64)
for provider in ("github", "google", "apple")}
super().__init__("pair to connect", "connect.comma.ai", self._comma_icon)
def _update_state(self):
super()._update_state()
self._show_commacare = (ui_state.prime_state.is_paired() and ui_state.prime_state.is_prime()
and ui_state.prime_state.has_commacare())
if ui_state.prime_state.is_paired():
self.set_icon(self._provider_icons.get(ui_state.prime_state.get_pairing_provider(), self._comma_icon))
self.set_text("paired")
if ui_state.prime_state.is_prime():
self.set_value("prime" if ui_state.prime_state.is_full_prime() else "prime lite")
else:
self.set_value("claim prime trial" if ui_state.prime_state.can_claim_prime_trial() else "upgrade to prime")
else:
self.set_icon(self._comma_icon)
self.set_text("pair to connect")
self.set_value("connect.comma.ai")
def _draw_subtitle(self, rect: rl.Rectangle):
if self._show_commacare:
y = rect.y + rect.height - self._commacare_badge.height
rl.draw_texture_v(self._commacare_badge, rl.Vector2(rect.x, y), rl.WHITE)
offset = self._commacare_badge.width + 14
rect = rl.Rectangle(rect.x + offset, rect.y, rect.width - offset, rect.height)
super()._draw_subtitle(rect)
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
dlg: BigDialog | PrimeScroller
if not system_time_valid():
dlg = BigDialog("", tr("Please connect to Wi-Fi to complete initial pairing."))
elif UNREGISTERED_DONGLE_ID == (ui_state.params.get("DongleId") or UNREGISTERED_DONGLE_ID):
dlg = BigDialog("", tr("Device must be registered with the comma.ai backend to pair."))
else:
dlg = PrimeScroller()
gui_app.push_widget(dlg)
class DeviceLayoutMici(NavScroller):
def __init__(self):
super().__init__()
self._fcc_dialog: MiciFccModal | None = None
self._pairing_button = PairBigButton()
self._pending_pairing_grow_animation = False
def power_off_callback():
ui_state.params.put_bool("DoShutdown", True, block=True)
def reboot_callback():
ui_state.params.put_bool("DoReboot", True, block=True)
reset_calibration_btn = EngagedConfirmationButton("reset calibration", "reset", gui_app.texture("icons_mici/settings/device/lkas.png", 122, 64),
self._reset_calibration,
description="Mount the device within 4° left or right and 5° up or 9° down. openpilot calibrates " +
"continuously; resetting is rarely needed. Resetting clears learned calibration.")
reset_calibration_btn.set_enabled(lambda: not ui_state.engaged and not calibration_reset.pending
and not ui_state.params.get_bool("OnroadCycleRequested"))
reboot_btn = EngagedConfirmationCircleButton("reboot", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70),
reboot_callback, exit_on_confirm=False)
self._power_off_btn = EngagedConfirmationCircleButton("power off", gui_app.texture("icons_mici/settings/device/power.png", 64, 66),
power_off_callback, exit_on_confirm=False, red=True)
self._power_off_btn.set_visible(lambda: not ui_state.ignition)
regulatory_btn = BigButton("regulatory info", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64))
regulatory_btn.set_click_callback(self._on_regulatory)
cabin_cam_btn = BigButton("driver\ncamera preview", "", gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64),
description="Preview the cabin camera to check driver monitoring visibility. The vehicle must be off.")
cabin_cam_btn.set_click_callback(lambda: gui_app.push_widget(CabinCameraDialog()))
cabin_cam_btn.set_enabled(lambda: ui_state.is_offroad())
reset_dm_btn = EngagedConfirmationButton("reset driver\nmonitoring", "reset driver monitoring",
gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64),
self._reset_driver_monitoring,
description="Clear saved wheel-side detection. Automatic detection restarts next drive.")
reset_dm_btn.set_enabled(self._can_reset_driver_monitoring)
review_training_guide_btn = BigButton("review\ntraining guide", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64))
review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTrainingGuide(completed_callback=lambda: gui_app.pop_widgets_to(self))))
review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad())
terms_btn = BigButton("terms &\nconditions", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64))
terms_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTermsPage()))
self._scroller.add_widgets([
DeviceInfoLayoutMici(),
self._pairing_button,
review_training_guide_btn,
cabin_cam_btn,
reset_dm_btn,
terms_btn,
regulatory_btn,
reset_calibration_btn,
reboot_btn,
self._power_off_btn,
])
def _can_reset_driver_monitoring(self) -> bool:
return native_parked(ui_state) and not ui_state.params.get_bool("IsDriverViewEnabled")
def _reset_calibration(self) -> None:
if not ui_state.engaged and isinstance(ui_state.params, Params):
calibration_reset.request(ui_state.params)
def _reset_driver_monitoring(self) -> None:
if not self._can_reset_driver_monitoring():
return
ui_state.params.remove("IsRhdDetected")
ui_state.params.put_bool("OnroadCycleRequested", True, block=True)
def _update_state(self):
super()._update_state()
if error := calibration_reset.take_error():
gui_app.push_widget(BigDialog("", error))
if self._pending_pairing_grow_animation:
btn_right = self._pairing_button.rect.x + self._pairing_button.rect.width
visible_right = self._rect.x + self._rect.width
if btn_right < visible_right + 50:
self._pending_pairing_grow_animation = False
self._pairing_button.trigger_grow_animation()
def show_event(self):
super().show_event()
if not ui_state.prime_state.is_paired():
self.set_shown_callback(self._pairing_button.trigger_grow_animation)
def hide_event(self):
super().hide_event()
self.set_shown_callback(None)
self._pending_pairing_grow_animation = False
def scroll_to_pairing(self):
self._scroller._layout()
offset = (self._pairing_button.rect.x + self._pairing_button.rect.width / 2) - (self._rect.x + self._rect.width / 2)
self._scroller.scroll_to(offset, smooth=True, block_interrupt=True, block_widget_interaction=True)
self._pending_pairing_grow_animation = True
def _on_regulatory(self):
if not self._fcc_dialog:
self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "openpilot/selfdrive/assets/offroad/mici_fcc.html"))
gui_app.push_widget(self._fcc_dialog)