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("

Steering lag calibration is {}% complete.").format(lag_perc) else: desc += tr("

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 += "

" 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())