Files
StarPilot/openpilot/selfdrive/ui/layouts/settings/device.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

214 lines
10 KiB
Python

import os
import math
from collections.abc import Callable
from openpilot.cereal import messaging, log
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.starpilot.schema_cache import get_cache
from openpilot.starpilot.ui.calibration_reset import calibration_reset
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.onroad.cabin_camera_dialog import CabinCameraDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.layouts.onboarding import TrainingGuide
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.lib.multilang import multilang, tr, tr_noop
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
from openpilot.system.ui.widgets.html_render import HtmlModal
from openpilot.system.ui.widgets.list_view import text_item, button_item, dual_button_item
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.system.ui.widgets.scroller_tici import Scroller
# Description constants
DESCRIPTIONS = {
'pair_device': tr_noop("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer."),
'cabin_camera': tr_noop("Preview the cabin camera to ensure that driver monitoring has good visibility. (vehicle must be off)"),
'reset_driver_monitoring': tr_noop("Clears the saved driver monitoring wheel-side calibration."),
'reset_calibration': tr_noop("openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."),
'review_guide': tr_noop("Review the rules, features, and limitations of openpilot"),
}
class DeviceLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
self._select_language_dialog: MultiOptionDialog | None = None
self._fcc_dialog: HtmlModal | None = None
self._calibration_reset_pending = False
items = self._initialize_items()
self._scroller = self._child(Scroller(items, line_separator=True, spacing=0))
ui_state.add_offroad_transition_callback(self._offroad_transition)
def _initialize_items(self):
self._pair_device_btn = button_item(lambda: tr("Pair Device"), lambda: tr("PAIR"), lambda: tr(DESCRIPTIONS['pair_device']),
callback=lambda: gui_app.push_widget(PairingDialog()))
self._pair_device_btn.set_visible(lambda: not ui_state.prime_state.is_paired())
self._reset_calib_btn = button_item(lambda: tr("Reset Calibration"), lambda: tr("RESET"), lambda: tr(DESCRIPTIONS['reset_calibration']),
callback=self._reset_calibration_prompt)
self._reset_calib_btn.set_description_opened_callback(self._update_calib_description)
self._reset_calib_btn.set_enabled(lambda: not ui_state.engaged and not calibration_reset.pending
and not self._params.get_bool("OnroadCycleRequested"))
self._power_off_btn = dual_button_item(lambda: tr("Reboot"), lambda: tr("Power Off"),
left_callback=self._reboot_prompt, right_callback=self._power_off_prompt)
items = [
text_item(lambda: tr("Dongle ID"), self._params.get("DongleId") or (lambda: tr("N/A"))),
text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))),
self._pair_device_btn,
button_item(lambda: tr("Cabin Camera"), lambda: tr("PREVIEW"), lambda: tr(DESCRIPTIONS['cabin_camera']),
callback=lambda: gui_app.push_widget(CabinCameraDialog()), enabled=ui_state.is_offroad),
button_item(lambda: tr("Reset Driver Monitoring"), lambda: tr("RESET"), lambda: tr(DESCRIPTIONS['reset_driver_monitoring']),
callback=self._reset_driver_monitoring_prompt, enabled=self._can_reset_driver_monitoring),
self._reset_calib_btn,
button_item(lambda: tr("Review Training Guide"), lambda: tr("REVIEW"), lambda: tr(DESCRIPTIONS['review_guide']),
self._on_review_training_guide, enabled=ui_state.is_offroad),
button_item(lambda: tr("Regulatory"), lambda: tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad),
button_item(lambda: tr("Change Language"), lambda: tr("CHANGE"), callback=self._show_language_dialog),
self._power_off_btn,
]
return items
def _offroad_transition(self):
self._power_off_btn.action_item.right_button.set_visible(ui_state.is_offroad())
def _render(self, rect):
self._scroller.render(rect)
def _update_state(self):
super()._update_state()
if self._calibration_reset_pending and not calibration_reset.pending:
self._calibration_reset_pending = False
self._update_calib_description()
if error := calibration_reset.take_error():
gui_app.push_widget(alert_dialog(tr(error)))
def _show_language_dialog(self):
def handle_language_selection(result: DialogResult):
if result == DialogResult.CONFIRM and self._select_language_dialog:
selected_language = multilang.languages[self._select_language_dialog.selection]
multilang.change_language(selected_language)
self._update_calib_description()
self._select_language_dialog = None
self._select_language_dialog = MultiOptionDialog(tr("Select a language"), multilang.languages, multilang.codes[multilang.language],
option_font_weight=FontWeight.UNIFONT, callback=handle_language_selection)
gui_app.push_widget(self._select_language_dialog)
def _reset_calibration_prompt(self, action_guard: Callable[[], bool] | None = None):
if ui_state.engaged:
gui_app.push_widget(alert_dialog(tr("Disengage to Reset Calibration")))
return
def reset_calibration(result: DialogResult):
# Check engaged again in case it changed while the dialog was open
if ui_state.engaged or result != DialogResult.CONFIRM or (action_guard is not None and not action_guard()):
return
self._calibration_reset_pending = calibration_reset.request(self._params) or self._calibration_reset_pending
dialog = ConfirmDialog(tr("Are you sure you want to reset calibration?"), tr("Reset"), callback=reset_calibration)
gui_app.push_widget(dialog)
def _can_reset_driver_monitoring(self) -> bool:
return ui_state.is_offroad() and not ui_state.engaged and not self._params.get_bool("IsDriverViewEnabled")
def _reset_driver_monitoring_prompt(self):
if not self._can_reset_driver_monitoring():
return
def reset_driver_monitoring(result: DialogResult):
if result == DialogResult.CONFIRM and self._can_reset_driver_monitoring():
self._params.remove("IsRhdDetected")
self._params.put_bool("OnroadCycleRequested", True, block=True)
gui_app.push_widget(ConfirmDialog(tr("Are you sure you want to reset driver monitoring calibration?"), tr("Reset"),
callback=reset_driver_monitoring))
def _update_calib_description(self):
desc = tr(DESCRIPTIONS['reset_calibration'])
calib_bytes = get_cache(self._params, "CalibrationParams")
if calib_bytes:
try:
calib = messaging.log_from_bytes(calib_bytes, log.Event).extrinsicsCalibration
if calib.calStatus != log.ExtrinsicsCalibration.Status.uncalibrated:
pitch = math.degrees(calib.rpyCalib[1])
yaw = math.degrees(calib.rpyCalib[2])
desc += tr(" Your device is pointed {:.1f}° {} and {:.1f}° {}.").format(abs(pitch), tr("down") if pitch > 0 else tr("up"),
abs(yaw), tr("left") if yaw > 0 else tr("right"))
except Exception:
cloudlog.exception("invalid CalibrationParams")
lag_perc = 0
lag_bytes = get_cache(self._params, "LiveDelay")
if lag_bytes:
try:
lag_perc = messaging.log_from_bytes(lag_bytes, log.Event).lateralDelay.calPerc
except Exception:
cloudlog.exception("invalid LiveDelay")
if lag_perc < 100:
desc += tr("<br><br>Steering lag calibration is {}% complete.").format(lag_perc)
else:
desc += tr("<br><br>Steering lag calibration is complete.")
torque_bytes = get_cache(self._params, "LiveTorqueParameters")
if torque_bytes:
try:
torque = messaging.log_from_bytes(torque_bytes, log.Event).lateralTorqueParameters
# don't add for non-torque cars
if torque.useParams:
torque_perc = torque.calPerc
if torque_perc < 100:
desc += tr(" Steering torque response calibration is {}% complete.").format(torque_perc)
else:
desc += tr(" Steering torque response calibration is complete.")
except Exception:
cloudlog.exception("invalid LiveTorqueParameters")
desc += "<br><br>"
desc += tr("openpilot is continuously calibrating, resetting is rarely required. " +
"Resetting calibration will restart openpilot if the car is powered on.")
self._reset_calib_btn.set_description(desc)
def _reboot_prompt(self, action_guard: Callable[[], bool] | None = None):
if ui_state.engaged:
gui_app.push_widget(alert_dialog(tr("Disengage to Reboot")))
return
def perform_reboot(result: DialogResult):
if not ui_state.engaged and result == DialogResult.CONFIRM and (action_guard is None or action_guard()):
self._params.put_bool("DoReboot", True)
dialog = ConfirmDialog(tr("Are you sure you want to reboot?"), tr("Reboot"), callback=perform_reboot)
gui_app.push_widget(dialog)
def _power_off_prompt(self, action_guard: Callable[[], bool] | None = None):
if ui_state.engaged:
gui_app.push_widget(alert_dialog(tr("Disengage to Power Off")))
return
def perform_power_off(result: DialogResult):
if not ui_state.engaged and result == DialogResult.CONFIRM and (action_guard is None or action_guard()):
self._params.put_bool("DoShutdown", True)
dialog = ConfirmDialog(tr("Are you sure you want to power off?"), tr("Power Off"), callback=perform_power_off)
gui_app.push_widget(dialog)
def _on_regulatory(self):
if not self._fcc_dialog:
self._fcc_dialog = HtmlModal(os.path.join(BASEDIR, "openpilot/selfdrive/assets/offroad/fcc.html"))
gui_app.push_widget(self._fcc_dialog)
def _on_review_training_guide(self):
gui_app.push_widget(TrainingGuide())