diff --git a/selfdrive/ui/layouts/settings/developer.py b/selfdrive/ui/layouts/settings/developer.py
index 32d278ee9b..d3a829df81 100644
--- a/selfdrive/ui/layouts/settings/developer.py
+++ b/selfdrive/ui/layouts/settings/developer.py
@@ -70,8 +70,6 @@ class DeveloperLayout(Widget):
callback=self._on_alpha_long_enabled,
)
- self._alpha_long_toggle.set_description(self._alpha_long_toggle.description + " Changing this setting will restart openpilot if the car is powered on.")
-
items = [
self._adb_toggle,
self._ssh_toggle,
diff --git a/selfdrive/ui/layouts/settings/device.py b/selfdrive/ui/layouts/settings/device.py
index 62abd2b998..758200c0ad 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,10 +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."
- ),
+ '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",
}
@@ -49,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)
+ self._reset_calib_btn.set_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),
@@ -95,19 +98,68 @@ class DeviceLayout(Widget):
gui_app.set_modal_overlay(alert_dialog("Disengage to Reset Calibration"))
return
- dialog = ConfirmDialog("Are you sure you want to reset calibration?", "Reset")
- gui_app.set_modal_overlay(dialog, callback=self._reset_calibration)
+ def reset_calibration(result: int):
+ # Check engaged again in case it changed while the dialog was open
+ if ui_state.engaged or result != DialogResult.CONFIRM:
+ return
- def _reset_calibration(self, result: int):
- if ui_state.engaged or result != DialogResult.CONFIRM:
- return
+ self._params.remove("CalibrationParams")
+ self._params.remove("LiveTorqueParameters")
+ self._params.remove("LiveParameters")
+ self._params.remove("LiveParametersV2")
+ self._params.remove("LiveDelay")
+ self._params.put_bool("OnroadCycleRequested", True)
+ self._update_calib_description()
- self._params.remove("CalibrationParams")
- self._params.remove("LiveTorqueParameters")
- self._params.remove("LiveParameters")
- self._params.remove("LiveParametersV2")
- self._params.remove("LiveDelay")
- self._params.put_bool("OnroadCycleRequested", True)
+ 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):
+ 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:
diff --git a/system/ui/widgets/confirm_dialog.py b/system/ui/widgets/confirm_dialog.py
index 789ae53f3d..6d3e143b53 100644
--- a/system/ui/widgets/confirm_dialog.py
+++ b/system/ui/widgets/confirm_dialog.py
@@ -78,12 +78,12 @@ class ConfirmDialog(Widget):
self._confirm_button.render(confirm_button)
self._cancel_button.render(cancel_button)
else:
- centered_button_x = dialog_rect.x + (dialog_rect.width - button_width) / 2
- centered_confirm_button = rl.Rectangle(centered_button_x, button_y, button_width, BUTTON_HEIGHT)
- self._confirm_button.render(centered_confirm_button)
+ full_button_width = dialog_rect.width - 2 * MARGIN
+ full_confirm_button = rl.Rectangle(dialog_rect.x + MARGIN, button_y, full_button_width, BUTTON_HEIGHT)
+ self._confirm_button.render(full_confirm_button)
return self._dialog_result
-def alert_dialog(message: str, button_text: str = "OK"):
+def alert_dialog(message: str, button_text: str = "Ok"):
return ConfirmDialog(message, button_text, cancel_text="")
diff --git a/system/ui/widgets/list_view.py b/system/ui/widgets/list_view.py
index bbf66c6555..d203ec8139 100644
--- a/system/ui/widgets/list_view.py
+++ b/system/ui/widgets/list_view.py
@@ -268,6 +268,7 @@ class ListItem(Widget):
self._description = description
self.description_visible = description_visible
self.callback = callback
+ self.description_opened_callback: Callable | None = None
self.action_item = action_item
self.set_rect(rl.Rectangle(0, 0, ITEM_BASE_WIDTH, ITEM_BASE_HEIGHT))
@@ -280,6 +281,9 @@ class ListItem(Widget):
# Cached properties for performance
self._prev_description: str | None = self.description
+ def set_description_opened_callback(self, callback: Callable) -> None:
+ self.description_opened_callback = callback
+
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
super().set_touch_valid_callback(touch_callback)
if self.action_item:
@@ -302,6 +306,10 @@ class ListItem(Widget):
if self.description:
self.description_visible = not self.description_visible
+ # do callback first in case receiver changes description
+ if self.description_visible and self.description_opened_callback is not None:
+ self.description_opened_callback()
+
content_width = int(self._rect.width - ITEM_PADDING * 2)
self._rect.height = self.get_item_height(self._font, content_width)