diff --git a/selfdrive/ui/layouts/settings/device.py b/selfdrive/ui/layouts/settings/device.py index 3fa60bfc2b..9d5a6ca642 100644 --- a/selfdrive/ui/layouts/settings/device.py +++ b/selfdrive/ui/layouts/settings/device.py @@ -1,8 +1,11 @@ import os import json +import math +from cereal import messaging, log from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.layouts.onboarding import TrainingGuide @@ -20,11 +23,7 @@ from openpilot.system.ui.widgets.scroller import Scroller DESCRIPTIONS = { 'pair_device': "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.", 'driver_camera': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)", - 'reset_calibration': ( - "openpilot requires the device to be mounted within 4° left or right and within 5° " + - "up or 9° down. openpilot is continuously calibrating, resetting is rarely required. " + - "Resetting calibration will restart openpilot if the car is powered on." - ), + 'reset_calibration': "openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down.", 'review_guide': "Review the rules, features, and limitations of openpilot", } @@ -50,12 +49,15 @@ class DeviceLayout(Widget): self._pair_device_btn = button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device) self._pair_device_btn.set_visible(lambda: not ui_state.prime_state.is_paired()) + self._reset_calib_btn = button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt, + description_opened_callback=self._update_calib_description) + items = [ text_item("Dongle ID", dongle_id), text_item("Serial", serial), self._pair_device_btn, button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad), - button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt), + self._reset_calib_btn, button_item("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide), regulatory_btn := button_item("Regulatory", "VIEW", callback=self._on_regulatory), button_item("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad), @@ -107,11 +109,121 @@ class DeviceLayout(Widget): self._params.remove("LiveParametersV2") self._params.remove("LiveDelay") self._params.put_bool("OnroadCycleRequested", True) - # self._update_calib_description() + self._update_calib_description() dialog = ConfirmDialog("Are you sure you want to reset calibration?", "Reset") gui_app.set_modal_overlay(dialog, callback=reset_calibration) + def _update_calib_description(self): + """ + QString desc = tr("openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."); + std::string calib_bytes = params.get("CalibrationParams"); + if (!calib_bytes.empty()) { + try { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(calib_bytes.data(), calib_bytes.size())); + auto calib = cmsg.getRoot().getLiveCalibration(); + if (calib.getCalStatus() != cereal::LiveCalibrationData::Status::UNCALIBRATED) { + double pitch = calib.getRpyCalib()[1] * (180 / M_PI); + double yaw = calib.getRpyCalib()[2] * (180 / M_PI); + desc += tr(" Your device is pointed %1° %2 and %3° %4.") + .arg(QString::number(std::abs(pitch), 'g', 1), pitch > 0 ? tr("down") : tr("up"), + QString::number(std::abs(yaw), 'g', 1), yaw > 0 ? tr("left") : tr("right")); + } + } catch (kj::Exception) { + qInfo() << "invalid CalibrationParams"; + } + } + + int lag_perc = 0; + std::string lag_bytes = params.get("LiveDelay"); + if (!lag_bytes.empty()) { + try { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size())); + lag_perc = cmsg.getRoot().getLiveDelay().getCalPerc(); + } catch (kj::Exception) { + qInfo() << "invalid LiveDelay"; + } + } + if (lag_perc < 100) { + desc += tr("\n\nSteering lag calibration is %1% complete.").arg(lag_perc); + } else { + desc += tr("\n\nSteering lag calibration is complete."); + } + + std::string torque_bytes = params.get("LiveTorqueParameters"); + if (!torque_bytes.empty()) { + try { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(torque_bytes.data(), torque_bytes.size())); + auto torque = cmsg.getRoot().getLiveTorqueParameters(); + // don't add for non-torque cars + if (torque.getUseParams()) { + int torque_perc = torque.getCalPerc(); + if (torque_perc < 100) { + desc += tr(" Steering torque response calibration is %1% complete.").arg(torque_perc); + } else { + desc += tr(" Steering torque response calibration is complete."); + } + } + } catch (kj::Exception) { + qInfo() << "invalid LiveTorqueParameters"; + } + } + + desc += "\n\n"; + desc += tr("openpilot is continuously calibrating, resetting is rarely required. " + "Resetting calibration will restart openpilot if the car is powered on."); + resetCalibBtn->setDescription(desc); + """ + + desc = DESCRIPTIONS['reset_calibration'] + + calib_bytes = self._params.get("CalibrationParams") + if calib_bytes: + try: + calib = messaging.log_from_bytes(calib_bytes, log.Event).liveCalibration + + if calib.calStatus != log.LiveCalibrationData.Status.uncalibrated: + pitch = math.degrees(calib.rpyCalib[1]) + yaw = math.degrees(calib.rpyCalib[2]) + desc += f" Your device is pointed {abs(pitch):.1f}° {'down' if pitch > 0 else 'up'} and {abs(yaw):.1f}° {'left' if yaw > 0 else 'right'}." + except Exception: + cloudlog.exception("invalid CalibrationParams") + + lag_perc = 0 + lag_bytes = self._params.get("LiveDelay") + if lag_bytes: + try: + lag_perc = messaging.log_from_bytes(lag_bytes, log.Event).liveDelay.calPerc + except Exception: + cloudlog.exception("invalid LiveDelay") + if lag_perc < 100: + desc += f"

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

Steering lag calibration is complete." + + torque_bytes = self._params.get("LiveTorqueParameters") + if torque_bytes: + try: + torque = messaging.log_from_bytes(torque_bytes, log.Event).liveTorqueParameters + # don't add for non-torque cars + if torque.useParams: + torque_perc = torque.calPerc + if torque_perc < 100: + desc += f" Steering torque response calibration is {torque_perc}% complete." + else: + desc += " Steering torque response calibration is complete." + except Exception: + cloudlog.exception("invalid LiveTorqueParameters") + + desc += "

" + desc += ("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): if ui_state.engaged: gui_app.set_modal_overlay(alert_dialog("Disengage to Reboot")) diff --git a/system/ui/widgets/list_view.py b/system/ui/widgets/list_view.py index bbf66c6555..6e7b853037 100644 --- a/system/ui/widgets/list_view.py +++ b/system/ui/widgets/list_view.py @@ -260,7 +260,7 @@ class MultipleButtonAction(ItemAction): class ListItem(Widget): def __init__(self, title: str = "", icon: str | None = None, description: str | Callable[[], str] | None = None, - description_visible: bool = False, callback: Callable | None = None, + description_visible: bool = False, callback: Callable | None = None, description_opened_callback: Callable | None = None, action_item: ItemAction | None = None): super().__init__() self.title = title @@ -268,6 +268,7 @@ class ListItem(Widget): self._description = description self.description_visible = description_visible self.callback = callback + self.description_opened_callback = description_opened_callback self.action_item = action_item self.set_rect(rl.Rectangle(0, 0, ITEM_BASE_WIDTH, ITEM_BASE_HEIGHT)) @@ -302,6 +303,10 @@ class ListItem(Widget): if self.description: self.description_visible = not self.description_visible + # do callback first in case consumer changes description + if self.description_visible and self.description_opened_callback: + self.description_opened_callback() + content_width = int(self._rect.width - ITEM_PADDING * 2) self._rect.height = self.get_item_height(self._font, content_width) @@ -411,9 +416,11 @@ def toggle_item(title: str, description: str | Callable[[], str] | None = None, def button_item(title: str, button_text: str | Callable[[], str], description: str | Callable[[], str] | None = None, - callback: Callable | None = None, enabled: bool | Callable[[], bool] = True) -> ListItem: + callback: Callable | None = None, description_opened_callback: Callable | None = None, + enabled: bool | Callable[[], bool] = True) -> ListItem: action = ButtonAction(text=button_text, enabled=enabled) - return ListItem(title=title, description=description, action_item=action, callback=callback) + return ListItem(title=title, description=description, action_item=action, callback=callback, + description_opened_callback=description_opened_callback) def text_item(title: str, value: str | Callable[[], str], description: str | Callable[[], str] | None = None,