forked from IQ.Lvbs/IQ.Pilot
IQ.Pilot Prebuilt Release @ ab07000
This commit is contained in:
9
selfdrive/ui/layouts/settings/common.py
Normal file
9
selfdrive/ui/layouts/settings/common.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
|
||||
def restart_needed_callback(_=None):
|
||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||
155
selfdrive/ui/layouts/settings/developer.py
Normal file
155
selfdrive/ui/layouts/settings/developer.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.hardware.tici.usb_storage import apply_usb_storage_state
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.list_view import toggle_item
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
|
||||
if gui_app.iqpilot_ui():
|
||||
from openpilot.system.ui.iqwidgets.widgets.list_view import toggle_item
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
'enable_adb': tr_noop(
|
||||
"ADB (Android Debug Bridge) allows connecting to your device over USB or over the network."
|
||||
),
|
||||
'ssh_key': tr_noop(
|
||||
"Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " +
|
||||
"other than your own. An IQ.Pilot employee will NEVER ask you to add their GitHub username."
|
||||
),
|
||||
'usb_storage': tr_noop(
|
||||
"Exposes a snapshot of recent dashcam clips and logs as a USB drive when connected to a computer. " +
|
||||
"IQ.Pilot keeps running while this is enabled."
|
||||
),
|
||||
'long_maneuver': tr_noop(
|
||||
"Commands a scripted sequence of acceleration steps to measure longitudinal actuator response. " +
|
||||
"Requires IQ.Pilot longitudinal control. Only use on a clear, closed road."
|
||||
),
|
||||
'lat_maneuver': tr_noop(
|
||||
"Commands a scripted sequence of lateral acceleration steps to measure steering actuator response. " +
|
||||
"Only use on a straight, flat, clear road."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class DeveloperLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._is_release = self._params.get_bool("IsReleaseBranch")
|
||||
|
||||
# Build items and keep references for callbacks/state updates
|
||||
self._adb_toggle = toggle_item(
|
||||
lambda: tr("Enable ADB"),
|
||||
description=lambda: tr(DESCRIPTIONS["enable_adb"]),
|
||||
initial_state=self._params.get_bool("AdbEnabled"),
|
||||
callback=self._on_enable_adb,
|
||||
enabled=ui_state.is_offroad,
|
||||
)
|
||||
|
||||
self._usb_storage_toggle = toggle_item(
|
||||
lambda: tr("USB Storage"),
|
||||
description=lambda: tr(DESCRIPTIONS["usb_storage"]),
|
||||
initial_state=self._params.get_bool("UsbStorageEnabled"),
|
||||
callback=self._on_enable_usb_storage,
|
||||
enabled=ui_state.is_offroad,
|
||||
)
|
||||
|
||||
# SSH enable toggle + SSH key management
|
||||
self._ssh_toggle = toggle_item(
|
||||
lambda: tr("Enable SSH"),
|
||||
description="",
|
||||
initial_state=self._params.get_bool("SshEnabled"),
|
||||
callback=self._on_enable_ssh,
|
||||
)
|
||||
self._ssh_keys = ssh_key_item(lambda: tr("SSH Keys"), description=lambda: tr(DESCRIPTIONS["ssh_key"]))
|
||||
|
||||
self._long_maneuver_toggle = toggle_item(
|
||||
lambda: tr("Longitudinal Maneuver Mode"),
|
||||
description=lambda: tr(DESCRIPTIONS["long_maneuver"]),
|
||||
initial_state=self._params.get_bool("LongitudinalManeuverMode"),
|
||||
callback=self._on_long_maneuver_mode,
|
||||
)
|
||||
|
||||
self._lat_maneuver_toggle = toggle_item(
|
||||
lambda: tr("Lateral Maneuver Mode"),
|
||||
description=lambda: tr(DESCRIPTIONS["lat_maneuver"]),
|
||||
initial_state=self._params.get_bool("LateralManeuverMode"),
|
||||
callback=self._on_lat_maneuver_mode,
|
||||
)
|
||||
|
||||
self._on_enable_ui_debug(self._params.get_bool("ShowDebugInfo"))
|
||||
|
||||
self._scroller = Scroller([
|
||||
self._adb_toggle,
|
||||
self._usb_storage_toggle,
|
||||
self._ssh_toggle,
|
||||
self._ssh_keys,
|
||||
self._long_maneuver_toggle,
|
||||
self._lat_maneuver_toggle,
|
||||
], line_separator=True, spacing=0)
|
||||
|
||||
# Toggles should be not available to change in onroad state
|
||||
ui_state.add_offroad_transition_callback(self._update_toggles)
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
self._update_toggles()
|
||||
|
||||
def _update_toggles(self):
|
||||
ui_state.update_params()
|
||||
|
||||
for item in (self._long_maneuver_toggle, self._lat_maneuver_toggle):
|
||||
item.set_visible(not self._is_release)
|
||||
|
||||
if ui_state.CP is not None:
|
||||
self._long_maneuver_toggle.action_item.set_enabled(ui_state.has_longitudinal_control and ui_state.is_offroad())
|
||||
self._lat_maneuver_toggle.action_item.set_enabled(ui_state.is_offroad())
|
||||
else:
|
||||
self._long_maneuver_toggle.action_item.set_enabled(False)
|
||||
self._lat_maneuver_toggle.action_item.set_enabled(False)
|
||||
|
||||
# TODO: make a param control list item so we don't need to manage internal state as much here
|
||||
# refresh toggles from params to mirror external changes
|
||||
for key, item in (
|
||||
("AdbEnabled", self._adb_toggle),
|
||||
("UsbStorageEnabled", self._usb_storage_toggle),
|
||||
("SshEnabled", self._ssh_toggle),
|
||||
("LongitudinalManeuverMode", self._long_maneuver_toggle),
|
||||
("LateralManeuverMode", self._lat_maneuver_toggle),
|
||||
):
|
||||
item.action_item.set_state(self._params.get_bool(key))
|
||||
|
||||
def _on_enable_ui_debug(self, state: bool):
|
||||
self._params.put_bool("ShowDebugInfo", state)
|
||||
gui_app.set_show_touches(state)
|
||||
gui_app.set_show_fps(state)
|
||||
gui_app.set_show_mouse_coords(state)
|
||||
|
||||
def _on_enable_adb(self, state: bool):
|
||||
self._params.put_bool("AdbEnabled", state)
|
||||
|
||||
def _on_enable_usb_storage(self, state: bool):
|
||||
apply_usb_storage_state(state)
|
||||
|
||||
def _on_enable_ssh(self, state: bool):
|
||||
self._params.put_bool("SshEnabled", state)
|
||||
|
||||
def _on_long_maneuver_mode(self, state: bool):
|
||||
self._params.put_bool("LongitudinalManeuverMode", state)
|
||||
self._params.put_bool("JoystickDebugMode", False)
|
||||
self._params.put_bool("LateralManeuverMode", False)
|
||||
self._lat_maneuver_toggle.action_item.set_state(False)
|
||||
|
||||
def _on_lat_maneuver_mode(self, state: bool):
|
||||
self._params.put_bool("LateralManeuverMode", state)
|
||||
self._params.put_bool("JoystickDebugMode", False)
|
||||
self._params.put_bool("ExperimentalMode", False)
|
||||
self._params.put_bool("LongitudinalManeuverMode", False)
|
||||
self._long_maneuver_toggle.action_item.set_state(False)
|
||||
214
selfdrive/ui/layouts/settings/device.py
Normal file
214
selfdrive/ui/layouts/settings/device.py
Normal file
@@ -0,0 +1,214 @@
|
||||
import os
|
||||
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
|
||||
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
|
||||
from openpilot.iqpilot.konn3kt.registration import get_cached_dongle_id
|
||||
from openpilot.system.hardware import TICI
|
||||
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
|
||||
|
||||
if gui_app.iqpilot_ui():
|
||||
from openpilot.system.ui.iqwidgets.widgets.list_view import button_item
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
'pair_device': tr_noop("Pair your device in the Konn3kt app."),
|
||||
'driver_camera': tr_noop("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)"),
|
||||
'reset_calibration': tr_noop("IQ.Pilot 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 IQ.Pilot"),
|
||||
}
|
||||
|
||||
|
||||
class DeviceLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
self._select_language_dialog: MultiOptionDialog | None = None
|
||||
self._driver_camera: DriverCameraDialog | None = None
|
||||
self._pair_device_dialog: PairingDialog | None = None
|
||||
self._fcc_dialog: HtmlModal | None = None
|
||||
self._training_guide: TrainingGuide | None = None
|
||||
|
||||
items = self._initialize_items()
|
||||
self._scroller = 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=self._pair_device)
|
||||
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._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"), lambda: get_cached_dongle_id(self._params, prefer_readonly=True) or 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("Driver Camera"), lambda: tr("PREVIEW"), lambda: tr(DESCRIPTIONS['driver_camera']),
|
||||
callback=self._show_driver_camera, enabled=ui_state.is_offroad),
|
||||
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),
|
||||
regulatory_btn := 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,
|
||||
]
|
||||
regulatory_btn.set_visible(TICI)
|
||||
return items
|
||||
|
||||
def _offroad_transition(self):
|
||||
self._power_off_btn.action_item.right_button.set_visible(ui_state.is_offroad())
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _show_language_dialog(self):
|
||||
def handle_language_selection(result: int):
|
||||
if result == 1 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)
|
||||
gui_app.set_modal_overlay(self._select_language_dialog, callback=handle_language_selection)
|
||||
|
||||
def _show_driver_camera(self):
|
||||
if not self._driver_camera:
|
||||
self._driver_camera = DriverCameraDialog()
|
||||
|
||||
gui_app.set_modal_overlay(self._driver_camera, callback=lambda result: setattr(self, '_driver_camera', None))
|
||||
|
||||
def _reset_calibration_prompt(self):
|
||||
if ui_state.engaged:
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reset Calibration")))
|
||||
return
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
dialog = ConfirmDialog(tr("Are you sure you want to reset calibration?"), tr("Reset"))
|
||||
gui_app.set_modal_overlay(dialog, callback=reset_calibration)
|
||||
|
||||
def _update_calib_description(self):
|
||||
desc = tr(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 += 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 = 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 += tr("<br><br>Steering lag calibration is {}% complete.").format(lag_perc)
|
||||
else:
|
||||
desc += tr("<br><br>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 += 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("IQ.Pilot is continuously calibrating, resetting is rarely required. " +
|
||||
"Resetting calibration will restart IQ.Pilot 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(tr("Disengage to Reboot")))
|
||||
return
|
||||
|
||||
dialog = ConfirmDialog(tr("Are you sure you want to reboot?"), tr("Reboot"))
|
||||
gui_app.set_modal_overlay(dialog, callback=self._perform_reboot)
|
||||
|
||||
def _perform_reboot(self, result: int):
|
||||
if not ui_state.engaged and result == DialogResult.CONFIRM:
|
||||
self._params.put_bool_nonblocking("DoReboot", True)
|
||||
|
||||
def _power_off_prompt(self):
|
||||
if ui_state.engaged:
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Power Off")))
|
||||
return
|
||||
|
||||
dialog = ConfirmDialog(tr("Are you sure you want to power off?"), tr("Power Off"))
|
||||
gui_app.set_modal_overlay(dialog, callback=self._perform_power_off)
|
||||
|
||||
def _perform_power_off(self, result: int):
|
||||
if not ui_state.engaged and result == DialogResult.CONFIRM:
|
||||
self._params.put_bool_nonblocking("DoShutdown", True)
|
||||
|
||||
def _pair_device(self):
|
||||
if not self._pair_device_dialog:
|
||||
self._pair_device_dialog = PairingDialog()
|
||||
gui_app.set_modal_overlay(self._pair_device_dialog, callback=lambda result: setattr(self, '_pair_device_dialog', None))
|
||||
|
||||
def _on_regulatory(self):
|
||||
if not self._fcc_dialog:
|
||||
self._fcc_dialog = HtmlModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html"))
|
||||
gui_app.set_modal_overlay(self._fcc_dialog)
|
||||
|
||||
def _on_review_training_guide(self):
|
||||
if not self._training_guide:
|
||||
def completed_callback():
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
self._training_guide = TrainingGuide(completed_callback=completed_callback)
|
||||
gui_app.set_modal_overlay(self._training_guide)
|
||||
170
selfdrive/ui/layouts/settings/settings.py
Normal file
170
selfdrive/ui/layouts/settings/settings.py
Normal file
@@ -0,0 +1,170 @@
|
||||
import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.network import NetworkUI
|
||||
|
||||
# Constants
|
||||
SIDEBAR_WIDTH = 500
|
||||
CLOSE_BTN_SIZE = 200
|
||||
CLOSE_ICON_SIZE = 70
|
||||
NAV_BTN_HEIGHT = 110
|
||||
PANEL_MARGIN = 50
|
||||
|
||||
# Colors
|
||||
SIDEBAR_COLOR = rl.BLACK
|
||||
PANEL_COLOR = rl.Color(41, 41, 41, 255)
|
||||
CLOSE_BTN_COLOR = rl.Color(41, 41, 41, 255)
|
||||
CLOSE_BTN_PRESSED = rl.Color(59, 59, 59, 255)
|
||||
TEXT_NORMAL = rl.Color(128, 128, 128, 255)
|
||||
TEXT_SELECTED = rl.WHITE
|
||||
|
||||
|
||||
class PanelType(IntEnum):
|
||||
DEVICE = 0
|
||||
NETWORK = 1
|
||||
TOGGLES = 2
|
||||
SOFTWARE = 3
|
||||
DEVELOPER = 5
|
||||
|
||||
|
||||
@dataclass
|
||||
class PanelInfo:
|
||||
name: str
|
||||
instance: Widget
|
||||
button_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
|
||||
class SettingsLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._current_panel = PanelType.DEVICE
|
||||
|
||||
# Panel configuration
|
||||
wifi_manager = WifiManager()
|
||||
wifi_manager.set_active(False)
|
||||
|
||||
self._panels = {
|
||||
PanelType.DEVICE: PanelInfo(tr_noop("Device"), DeviceLayout()),
|
||||
PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUI(wifi_manager)),
|
||||
PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout()),
|
||||
PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout()),
|
||||
PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout()),
|
||||
}
|
||||
|
||||
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
||||
self._close_icon = gui_app.texture("icons/close2.png", CLOSE_ICON_SIZE, CLOSE_ICON_SIZE)
|
||||
|
||||
# Callbacks
|
||||
self._close_callback: Callable | None = None
|
||||
|
||||
def set_callbacks(self, on_close: Callable):
|
||||
self._close_callback = on_close
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# Calculate layout
|
||||
sidebar_rect = rl.Rectangle(rect.x, rect.y, SIDEBAR_WIDTH, rect.height)
|
||||
panel_rect = rl.Rectangle(rect.x + SIDEBAR_WIDTH, rect.y, rect.width - SIDEBAR_WIDTH, rect.height)
|
||||
|
||||
# Draw components
|
||||
self._draw_sidebar(sidebar_rect)
|
||||
self._draw_current_panel(panel_rect)
|
||||
|
||||
def _draw_sidebar(self, rect: rl.Rectangle):
|
||||
rl.draw_rectangle_rec(rect, SIDEBAR_COLOR)
|
||||
|
||||
# Close button
|
||||
close_btn_rect = rl.Rectangle(
|
||||
rect.x + (rect.width - CLOSE_BTN_SIZE) / 2, rect.y + 60, CLOSE_BTN_SIZE, CLOSE_BTN_SIZE
|
||||
)
|
||||
|
||||
pressed = (rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and
|
||||
rl.check_collision_point_rec(rl.get_mouse_position(), close_btn_rect))
|
||||
close_color = CLOSE_BTN_PRESSED if pressed else CLOSE_BTN_COLOR
|
||||
rl.draw_rectangle_rounded(close_btn_rect, 1.0, 20, close_color)
|
||||
|
||||
icon_color = rl.Color(255, 255, 255, 255) if not pressed else rl.Color(220, 220, 220, 255)
|
||||
icon_dest = rl.Rectangle(
|
||||
close_btn_rect.x + (close_btn_rect.width - self._close_icon.width) / 2,
|
||||
close_btn_rect.y + (close_btn_rect.height - self._close_icon.height) / 2,
|
||||
self._close_icon.width,
|
||||
self._close_icon.height,
|
||||
)
|
||||
rl.draw_texture_pro(
|
||||
self._close_icon,
|
||||
rl.Rectangle(0, 0, self._close_icon.width, self._close_icon.height),
|
||||
icon_dest,
|
||||
rl.Vector2(0, 0),
|
||||
0,
|
||||
icon_color,
|
||||
)
|
||||
|
||||
# Store close button rect for click detection
|
||||
self._close_btn_rect = close_btn_rect
|
||||
|
||||
# Navigation buttons
|
||||
y = rect.y + 300
|
||||
for panel_type, panel_info in self._panels.items():
|
||||
button_rect = rl.Rectangle(rect.x + 50, y, rect.width - 150, NAV_BTN_HEIGHT)
|
||||
|
||||
# Button styling
|
||||
is_selected = panel_type == self._current_panel
|
||||
text_color = TEXT_SELECTED if is_selected else TEXT_NORMAL
|
||||
# Draw button text (right-aligned)
|
||||
panel_name = tr(panel_info.name)
|
||||
text_size = measure_text_cached(self._font_medium, panel_name, 65)
|
||||
text_pos = rl.Vector2(
|
||||
button_rect.x + button_rect.width - text_size.x, button_rect.y + (button_rect.height - text_size.y) / 2
|
||||
)
|
||||
rl.draw_text_ex(self._font_medium, panel_name, text_pos, 65, 0, text_color)
|
||||
|
||||
# Store button rect for click detection
|
||||
panel_info.button_rect = button_rect
|
||||
|
||||
y += NAV_BTN_HEIGHT
|
||||
|
||||
def _draw_current_panel(self, rect: rl.Rectangle):
|
||||
rl.draw_rectangle_rounded(
|
||||
rl.Rectangle(rect.x + 10, rect.y + 10, rect.width - 20, rect.height - 20), 0.04, 30, PANEL_COLOR
|
||||
)
|
||||
content_rect = rl.Rectangle(rect.x + PANEL_MARGIN, rect.y + 25, rect.width - (PANEL_MARGIN * 2), rect.height - 50)
|
||||
# rl.draw_rectangle_rounded(content_rect, 0.03, 30, PANEL_COLOR)
|
||||
panel = self._panels[self._current_panel]
|
||||
if panel.instance:
|
||||
panel.instance.render(content_rect)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos) -> None:
|
||||
# Check close button
|
||||
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
|
||||
if self._close_callback:
|
||||
self._close_callback()
|
||||
return
|
||||
|
||||
# Check navigation buttons
|
||||
for panel_type, panel_info in self._panels.items():
|
||||
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect):
|
||||
self.set_current_panel(panel_type)
|
||||
return
|
||||
|
||||
def set_current_panel(self, panel_type: PanelType):
|
||||
if panel_type != self._current_panel:
|
||||
self._panels[self._current_panel].instance.hide_event()
|
||||
self._current_panel = panel_type
|
||||
self._panels[self._current_panel].instance.show_event()
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._panels[self._current_panel].instance.show_event()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._panels[self._current_panel].instance.hide_event()
|
||||
262
selfdrive/ui/layouts/settings/software.py
Normal file
262
selfdrive/ui/layouts/settings/software.py
Normal file
@@ -0,0 +1,262 @@
|
||||
import os
|
||||
import time
|
||||
import datetime
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr, trn
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.widgets.list_view import button_item, text_item, ListItem
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
if gui_app.iqpilot_ui():
|
||||
from openpilot.system.ui.iqwidgets.widgets.list_view import button_item
|
||||
|
||||
# TODO: remove this. updater fails to respond on startup if time is not correct
|
||||
UPDATED_TIMEOUT = 10 # seconds to wait for updated to respond
|
||||
BRAND_NAME = "IQ.Pilot"
|
||||
|
||||
# Mapping updater internal states to translated display strings
|
||||
STATE_TO_DISPLAY_TEXT = {
|
||||
"checking...": tr("checking..."),
|
||||
"downloading...": tr("downloading..."),
|
||||
"finalizing update...": tr("finalizing update..."),
|
||||
}
|
||||
|
||||
|
||||
def format_updater_description(description: str | None) -> str:
|
||||
if not description:
|
||||
return BRAND_NAME
|
||||
|
||||
cleaned = description.strip()
|
||||
lower = cleaned.lower()
|
||||
if lower.startswith("iqpilot"):
|
||||
cleaned = cleaned[len("iqpilot"):].lstrip(" -:/")
|
||||
|
||||
if cleaned.lower().startswith(BRAND_NAME.lower()):
|
||||
return cleaned
|
||||
return f"{BRAND_NAME} {cleaned}" if cleaned else BRAND_NAME
|
||||
|
||||
|
||||
def time_ago(date: datetime.datetime | None) -> str:
|
||||
if not date:
|
||||
return tr("never")
|
||||
|
||||
if not system_time_valid():
|
||||
return date.strftime("%a %b %d %Y")
|
||||
|
||||
now = datetime.datetime.now(datetime.UTC)
|
||||
if date.tzinfo is None:
|
||||
date = date.replace(tzinfo=datetime.UTC)
|
||||
|
||||
diff_seconds = int((now - date).total_seconds())
|
||||
if diff_seconds < 60:
|
||||
return tr("now")
|
||||
if diff_seconds < 3600:
|
||||
m = diff_seconds // 60
|
||||
return trn("{} minute ago", "{} minutes ago", m).format(m)
|
||||
if diff_seconds < 86400:
|
||||
h = diff_seconds // 3600
|
||||
return trn("{} hour ago", "{} hours ago", h).format(h)
|
||||
if diff_seconds < 604800:
|
||||
d = diff_seconds // 86400
|
||||
return trn("{} day ago", "{} days ago", d).format(d)
|
||||
return date.strftime("%a %b %d %Y")
|
||||
|
||||
|
||||
class SoftwareLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._onroad_label = ListItem(lambda: tr("Updates are only downloaded while the car is off."))
|
||||
self._version_item = text_item(lambda: tr("Current Version"), format_updater_description(ui_state.params.get("UpdaterCurrentDescription")))
|
||||
self._download_btn = button_item(lambda: tr("Download"), lambda: tr("CHECK"), callback=self._on_download_update)
|
||||
|
||||
# Install button is initially hidden
|
||||
self._install_btn = button_item(lambda: tr("Install Update"), lambda: tr("INSTALL"), callback=self._on_install_update)
|
||||
self._install_btn.set_visible(False)
|
||||
|
||||
# Track waiting-for-updater transition to avoid brief re-enable while still idle
|
||||
self._waiting_for_updater = False
|
||||
self._waiting_start_ts: float = 0.0
|
||||
|
||||
# Branch switcher
|
||||
self._branch_btn = button_item(lambda: tr("Target Branch"), lambda: tr("SELECT"), callback=self._on_select_branch)
|
||||
self._branch_btn.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
|
||||
self._branch_btn.action_item.set_value(ui_state.params.get("UpdaterTargetBranch") or "")
|
||||
self._branch_dialog: MultiOptionDialog | None = None
|
||||
|
||||
# Git auth for private-branch updates (sits with the Target Branch section)
|
||||
self._auth_btn = button_item(lambda: tr("Git Auth"), lambda: tr("AUTH"), callback=self._on_auth_branch)
|
||||
self._auth_btn.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
|
||||
|
||||
self._scroller = Scroller([
|
||||
self._onroad_label,
|
||||
self._version_item,
|
||||
self._download_btn,
|
||||
self._install_btn,
|
||||
self._branch_btn,
|
||||
self._auth_btn,
|
||||
button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall),
|
||||
], line_separator=True, spacing=0)
|
||||
|
||||
def show_event(self):
|
||||
self._refresh_auth_value()
|
||||
self._scroller.show_event()
|
||||
|
||||
def _refresh_auth_value(self):
|
||||
try:
|
||||
from openpilot.common.git_creds import has_credentials
|
||||
configured = has_credentials()
|
||||
except Exception:
|
||||
configured = False
|
||||
self._auth_btn.action_item.set_value(tr("configured") if configured else "")
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _update_state(self):
|
||||
# Show/hide onroad warning
|
||||
self._onroad_label.set_visible(ui_state.is_onroad())
|
||||
|
||||
# Update current version and release notes
|
||||
current_desc = format_updater_description(ui_state.params.get("UpdaterCurrentDescription"))
|
||||
current_release_notes = (ui_state.params.get("UpdaterCurrentReleaseNotes") or b"").decode("utf-8", "replace")
|
||||
self._version_item.action_item.set_text(current_desc)
|
||||
self._version_item.set_description(current_release_notes)
|
||||
|
||||
# Update download button visibility and state
|
||||
self._download_btn.set_visible(ui_state.is_offroad())
|
||||
|
||||
updater_state = ui_state.params.get("UpdaterState") or "idle"
|
||||
failed_count = ui_state.params.get("UpdateFailedCount") or 0
|
||||
fetch_available = ui_state.params.get_bool("UpdaterFetchAvailable")
|
||||
update_available = ui_state.params.get_bool("UpdateAvailable")
|
||||
|
||||
if updater_state != "idle":
|
||||
# Updater responded
|
||||
self._waiting_for_updater = False
|
||||
self._download_btn.action_item.set_enabled(False)
|
||||
# Use the mapping, with a fallback to the original state string
|
||||
display_text = STATE_TO_DISPLAY_TEXT.get(updater_state, updater_state)
|
||||
self._download_btn.action_item.set_value(display_text)
|
||||
else:
|
||||
if failed_count > 0:
|
||||
self._download_btn.action_item.set_value(tr("failed to check for update"))
|
||||
self._download_btn.action_item.set_text(tr("CHECK"))
|
||||
elif fetch_available:
|
||||
self._download_btn.action_item.set_value(tr("update available"))
|
||||
self._download_btn.action_item.set_text(tr("DOWNLOAD"))
|
||||
else:
|
||||
last_update = ui_state.params.get("LastUpdateTime")
|
||||
if last_update:
|
||||
formatted = time_ago(last_update)
|
||||
self._download_btn.action_item.set_value(tr("up to date, last checked {}").format(formatted))
|
||||
else:
|
||||
self._download_btn.action_item.set_value(tr("up to date, last checked never"))
|
||||
self._download_btn.action_item.set_text(tr("CHECK"))
|
||||
|
||||
# If we've been waiting too long without a state change, reset state
|
||||
if self._waiting_for_updater and (time.monotonic() - self._waiting_start_ts > UPDATED_TIMEOUT):
|
||||
self._waiting_for_updater = False
|
||||
|
||||
# Only enable if we're not waiting for updater to flip out of idle
|
||||
self._download_btn.action_item.set_enabled(not self._waiting_for_updater)
|
||||
|
||||
# Update target branch button value
|
||||
current_branch = ui_state.params.get("UpdaterTargetBranch") or ""
|
||||
self._branch_btn.action_item.set_value(current_branch)
|
||||
|
||||
# Update install button
|
||||
self._install_btn.set_visible(ui_state.is_offroad() and update_available)
|
||||
if update_available:
|
||||
new_desc = format_updater_description(ui_state.params.get("UpdaterNewDescription"))
|
||||
new_release_notes = (ui_state.params.get("UpdaterNewReleaseNotes") or b"").decode("utf-8", "replace")
|
||||
self._install_btn.action_item.set_text(tr("INSTALL"))
|
||||
self._install_btn.action_item.set_value(new_desc)
|
||||
self._install_btn.set_description(new_release_notes)
|
||||
# Enable install button for testing (like Qt showEvent)
|
||||
self._install_btn.action_item.set_enabled(True)
|
||||
else:
|
||||
self._install_btn.set_visible(False)
|
||||
|
||||
def _on_download_update(self):
|
||||
# Check if we should start checking or start downloading
|
||||
self._download_btn.action_item.set_enabled(False)
|
||||
if self._download_btn.action_item.text == tr("CHECK"):
|
||||
# Start checking for updates
|
||||
self._waiting_for_updater = True
|
||||
self._waiting_start_ts = time.monotonic()
|
||||
os.system("pkill -SIGUSR1 -f system.updated.updated")
|
||||
else:
|
||||
# Start downloading
|
||||
self._waiting_for_updater = True
|
||||
self._waiting_start_ts = time.monotonic()
|
||||
os.system("pkill -SIGHUP -f system.updated.updated")
|
||||
|
||||
def _on_uninstall(self):
|
||||
def handle_uninstall_confirmation(result):
|
||||
if result == DialogResult.CONFIRM:
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
|
||||
dialog = ConfirmDialog(tr("Are you sure you want to uninstall?"), tr("Uninstall"))
|
||||
gui_app.set_modal_overlay(dialog, callback=handle_uninstall_confirmation)
|
||||
|
||||
def _on_install_update(self):
|
||||
# Trigger reboot to install update
|
||||
self._install_btn.action_item.set_enabled(False)
|
||||
ui_state.params.put_bool("DoReboot", True)
|
||||
|
||||
def _on_select_branch(self):
|
||||
# Get available branches and order
|
||||
current_git_branch = ui_state.params.get("GitBranch") or ""
|
||||
branches_str = ui_state.params.get("UpdaterAvailableBranches") or ""
|
||||
branches = [b for b in branches_str.split(",") if b]
|
||||
|
||||
for b in [current_git_branch, "devel-staging", "devel", "nightly", "nightly-dev", "master"]:
|
||||
if b in branches:
|
||||
branches.remove(b)
|
||||
branches.insert(0, b)
|
||||
|
||||
current_target = ui_state.params.get("UpdaterTargetBranch") or ""
|
||||
self._branch_dialog = MultiOptionDialog(tr("Select a branch"), branches, current_target)
|
||||
|
||||
def handle_selection(result):
|
||||
# Confirmed selection
|
||||
if result == DialogResult.CONFIRM and self._branch_dialog is not None and self._branch_dialog.selection:
|
||||
selection = self._branch_dialog.selection
|
||||
ui_state.params.put("UpdaterTargetBranch", selection)
|
||||
self._branch_btn.action_item.set_value(selection)
|
||||
os.system("pkill -SIGUSR1 -f system.updated.updated")
|
||||
self._branch_dialog = None
|
||||
|
||||
gui_app.set_modal_overlay(self._branch_dialog, callback=handle_selection)
|
||||
|
||||
def _on_auth_branch(self):
|
||||
# Collect username then token; store encrypted and signal the updater to
|
||||
# re-check (refreshing the available-branch list for private repos).
|
||||
from openpilot.system.ui.iqwidgets.widgets.list_view import open_text_prompt
|
||||
from openpilot.common import git_creds
|
||||
|
||||
creds = git_creds.get_credentials()
|
||||
current_user = creds[0] if creds else ""
|
||||
|
||||
def on_username(result, username):
|
||||
if result != DialogResult.CONFIRM:
|
||||
return
|
||||
|
||||
def on_token(token_result, token):
|
||||
if token_result != DialogResult.CONFIRM:
|
||||
return
|
||||
git_creds.set_credentials(username, token)
|
||||
self._refresh_auth_value()
|
||||
os.system("pkill -SIGUSR1 -f system.updated.updated")
|
||||
|
||||
open_text_prompt(tr("Git token / password"),
|
||||
tr("leave username and token blank to clear"),
|
||||
password=True, on_done=on_token)
|
||||
|
||||
open_text_prompt(tr("Git username"), tr("for private branch updates"),
|
||||
initial=current_user, on_done=on_username)
|
||||
343
selfdrive/ui/layouts/settings/toggles.py
Normal file
343
selfdrive/ui/layouts/settings/toggles.py
Normal file
@@ -0,0 +1,343 @@
|
||||
from cereal import log
|
||||
from openpilot.common.params import Params, UnknownKeyName
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
if gui_app.iqpilot_ui():
|
||||
from openpilot.system.ui.iqwidgets.widgets.list_view import toggle_item
|
||||
from openpilot.system.ui.iqwidgets.widgets.list_view import multiple_button_item
|
||||
from openpilot.iqpilot.ui.layouts.settings.iq_dynamic import IQDynamicLayout
|
||||
|
||||
PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants
|
||||
PERSONALITY_DISPLAY_TO_PARAM = [PERSONALITY_TO_INT["relaxed"], PERSONALITY_TO_INT["standard"], PERSONALITY_TO_INT["aggressive"]]
|
||||
PERSONALITY_PARAM_TO_DISPLAY = {param: idx for idx, param in enumerate(PERSONALITY_DISPLAY_TO_PARAM)}
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
"OpenpilotEnabledToggle": tr_noop(
|
||||
"Use the IQ.Pilot system for adaptive cruise control and lane keep driver assistance. " +
|
||||
"Your attention is required at all times to use this feature."
|
||||
),
|
||||
"DisengageOnAccelerator": tr_noop("When enabled, pressing the accelerator pedal will disengage IQ.Pilot."),
|
||||
"LongitudinalPersonality": tr_noop(
|
||||
"Standard is recommended. In aggressive mode, IQ.Pilot will follow lead cars closer and be more aggressive with the gas and brake. " +
|
||||
"In relaxed mode IQ.Pilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
|
||||
"your steering wheel distance button."
|
||||
),
|
||||
"IQSpeedAssistMode": tr_noop(
|
||||
"Controls IQ.Pilot speed limit behavior. Off disables speed limit features, Information only displays limits, Warning highlights overspeed, and Control adjusts set speed using detected limits."
|
||||
),
|
||||
"IsLdwEnabled": tr_noop(
|
||||
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
|
||||
"without a turn signal activated while driving over 31 mph (50 km/h)."
|
||||
),
|
||||
"AlwaysOnDM": tr_noop("Enable driver monitoring even when IQ.Pilot is not engaged."),
|
||||
"DashcamEnabled": tr_noop("Record and upload driving data and video. Disabling this stops all recording! No logs, no video, no audio."),
|
||||
'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."),
|
||||
"IsMetric": tr_noop("Display speed in km/h instead of mph."),
|
||||
"IQAutoUnits": tr_noop(
|
||||
"Set the units from the device location. Speeds switch to km/h everywhere except the United States, " +
|
||||
"the United Kingdom and Liberia, and are re-checked when you cross a border."
|
||||
),
|
||||
"RecordAudio": tr_noop("Record and store microphone audio while driving. The audio will be included in the dashcam video in Konn3kt."),
|
||||
"LongitudinalControlMode": tr_noop(
|
||||
"Choose longitudinal behavior: IQ.Pilot (IQ longitudinal + end-to-end), "
|
||||
"IQ.Dynamic (IQ longitudinal + dynamic mode), IQ.Standard (IQ longitudinal + relaxed personality), "
|
||||
"or Stock ACC."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class TogglesLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
# Keep IQ.Pilot enabled by default; the UI no longer exposes this toggle.
|
||||
self._params.put_bool("OpenpilotEnabledToggle", True)
|
||||
|
||||
# param, title, desc, icon, needs_restart
|
||||
self._toggle_defs = {
|
||||
"DisengageOnAccelerator": (
|
||||
lambda: tr("Disengage on Accelerator Pedal"),
|
||||
DESCRIPTIONS["DisengageOnAccelerator"],
|
||||
"disengage_on_accelerator.png",
|
||||
False,
|
||||
),
|
||||
"IsLdwEnabled": (
|
||||
lambda: tr("Enable Lane Departure Warnings"),
|
||||
DESCRIPTIONS["IsLdwEnabled"],
|
||||
"warning.png",
|
||||
False,
|
||||
),
|
||||
"DashcamEnabled": (
|
||||
lambda: tr("Enable Dashcam"),
|
||||
DESCRIPTIONS["DashcamEnabled"],
|
||||
"camera.png",
|
||||
True,
|
||||
),
|
||||
"RecordFront": (
|
||||
lambda: tr("Record and Upload Driver Camera"),
|
||||
DESCRIPTIONS["RecordFront"],
|
||||
"monitoring.png",
|
||||
True,
|
||||
),
|
||||
"RecordAudio": (
|
||||
lambda: tr("Record and Upload Microphone Audio"),
|
||||
DESCRIPTIONS["RecordAudio"],
|
||||
"microphone.png",
|
||||
True,
|
||||
),
|
||||
"IsMetric": (
|
||||
lambda: tr("Use Metric System"),
|
||||
DESCRIPTIONS["IsMetric"],
|
||||
"metric.png",
|
||||
False,
|
||||
),
|
||||
"IQAutoUnits": (
|
||||
lambda: tr("Set Units From Location"),
|
||||
DESCRIPTIONS["IQAutoUnits"],
|
||||
"metric.png",
|
||||
False,
|
||||
),
|
||||
}
|
||||
|
||||
self._long_personality_setting = multiple_button_item(
|
||||
lambda: tr("Driving Personality"),
|
||||
lambda: tr(DESCRIPTIONS["LongitudinalPersonality"]),
|
||||
buttons=[lambda: tr("Relaxed"), lambda: tr("Standard"), lambda: tr("Aggressive")],
|
||||
button_width=300,
|
||||
callback=self._set_longitudinal_personality,
|
||||
selected_index=PERSONALITY_PARAM_TO_DISPLAY.get(self._params.get("LongitudinalPersonality", return_default=True), 1),
|
||||
icon="speed_limit.png"
|
||||
)
|
||||
self._speed_limit_mode_setting = multiple_button_item(
|
||||
lambda: tr("Speed Limit"),
|
||||
lambda: tr(DESCRIPTIONS["IQSpeedAssistMode"]),
|
||||
buttons=[lambda: tr("Off"), lambda: tr("Info"), lambda: tr("Warning"), lambda: tr("Control")],
|
||||
button_width=220,
|
||||
callback=self._set_speed_limit_mode,
|
||||
selected_index=self._params.get("IQSpeedAssistMode", return_default=True),
|
||||
icon="speed_limit.png",
|
||||
)
|
||||
self._longitudinal_control_mode_setting = multiple_button_item(
|
||||
lambda: tr("Longitudinal Control"),
|
||||
lambda: tr(DESCRIPTIONS["LongitudinalControlMode"]),
|
||||
buttons=[lambda: tr("Stock ACC"), lambda: tr("IQ.Standard"), lambda: tr("IQ.Dynamic"), lambda: tr("IQ.Pilot")],
|
||||
button_width=250,
|
||||
callback=self._set_longitudinal_control_mode,
|
||||
selected_index=self._get_longitudinal_control_mode_index(),
|
||||
icon="experimental_white.png",
|
||||
)
|
||||
|
||||
self._toggles = {}
|
||||
self._locked_toggles = set()
|
||||
self._toggles["LongitudinalControlMode"] = self._longitudinal_control_mode_setting
|
||||
self._toggles["LongitudinalPersonality"] = self._long_personality_setting
|
||||
self._toggles["IQSpeedAssistMode"] = self._speed_limit_mode_setting
|
||||
|
||||
for param, (title, desc, icon, needs_restart) in self._toggle_defs.items():
|
||||
initial_state = self._params.get_bool(param)
|
||||
toggle = toggle_item(
|
||||
title,
|
||||
desc,
|
||||
initial_state,
|
||||
callback=lambda state, p=param: self._toggle_callback(state, p),
|
||||
icon=icon,
|
||||
)
|
||||
|
||||
try:
|
||||
locked = self._params.get_bool(param + "Lock")
|
||||
except UnknownKeyName:
|
||||
locked = False
|
||||
toggle.action_item.set_enabled(not locked)
|
||||
|
||||
# Make description callable for live translation
|
||||
additional_desc = ""
|
||||
if needs_restart and not locked:
|
||||
additional_desc = tr("Changing this setting will restart IQ.Pilot if the car is powered on.")
|
||||
toggle.set_description(lambda og_desc=toggle.description, add_desc=additional_desc: tr(og_desc) + (" " + tr(add_desc) if add_desc else ""))
|
||||
|
||||
# track for engaged state updates
|
||||
if locked:
|
||||
self._locked_toggles.add(param)
|
||||
|
||||
self._toggles[param] = toggle
|
||||
|
||||
self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0)
|
||||
|
||||
self._iq_dynamic_panel: "IQDynamicLayout | None" = None
|
||||
self._show_iq_dynamic = False
|
||||
if gui_app.iqpilot_ui():
|
||||
self._iq_dynamic_panel = IQDynamicLayout(self._close_iq_dynamic_panel)
|
||||
|
||||
ui_state.add_engaged_transition_callback(self._update_toggles)
|
||||
|
||||
def _update_state(self):
|
||||
if ui_state.sm.updated["selfdriveState"]:
|
||||
personality = PERSONALITY_TO_INT[ui_state.sm["selfdriveState"].personality]
|
||||
if personality != ui_state.personality and ui_state.started:
|
||||
self._long_personality_setting.action_item.set_selected_button(PERSONALITY_PARAM_TO_DISPLAY.get(personality, 1))
|
||||
ui_state.personality = personality
|
||||
self._speed_limit_mode_setting.action_item.set_selected_button(self._params.get("IQSpeedAssistMode", return_default=True))
|
||||
|
||||
def _close_iq_dynamic_panel(self):
|
||||
self._show_iq_dynamic = False
|
||||
|
||||
def set_cruise_panel_callback(self, callback: "Callable") -> None:
|
||||
"""Register callback invoked on double-click of IQ.Dynamic (button index 2)."""
|
||||
action = self._longitudinal_control_mode_setting.action_item
|
||||
if hasattr(action, 'set_double_click_callback'):
|
||||
action.set_double_click_callback(2, callback)
|
||||
|
||||
def show_event(self):
|
||||
self._show_iq_dynamic = False
|
||||
self._scroller.show_event()
|
||||
self._update_toggles()
|
||||
|
||||
def _update_toggles(self):
|
||||
ui_state.update_params()
|
||||
|
||||
e2e_description = tr(
|
||||
"Longitudinal Control modes:<br>" +
|
||||
"IQ.Pilot features are listed below:<br>" +
|
||||
"<h4>IQ.Pilot End-to-End Longitudinal Control</h4><br>" +
|
||||
"Let the driving model control the gas and brakes. IQ.Pilot will drive as it thinks a human would, including stopping for red lights and stop signs. " +
|
||||
"Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This feature is still being improved; " +
|
||||
"mistakes should be expected.<br>" +
|
||||
"<h4>IQ.Dynamic</h4><br>" +
|
||||
"Dynamically blends between adaptive cruise behavior and end-to-end behavior based on scene/context.<br>" +
|
||||
"<h4>IQ.Standard</h4><br>" +
|
||||
"Uses standard traffic-aware cruise behavior for longitudinal control.<br>" +
|
||||
"<h4>New Driving Visualization</h4><br>" +
|
||||
"The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. " +
|
||||
"The IQ.Pilot logo will also be shown in the top right corner."
|
||||
)
|
||||
|
||||
alpha_available = bool(ui_state.CP is not None and ui_state.CP.alphaLongitudinalAvailable)
|
||||
alpha_requested = self._params.get_bool("AlphaLongitudinalEnabled")
|
||||
toyota_stock_long_forced = bool(
|
||||
ui_state.CP is not None and
|
||||
ui_state.CP.brand == "toyota" and
|
||||
self._params.get_bool("IQToyotaFactoryLong")
|
||||
)
|
||||
iq_modes_selectable = alpha_available or alpha_requested or toyota_stock_long_forced
|
||||
availability_note = ""
|
||||
if ui_state.CP is None:
|
||||
availability_note = tr("Vehicle longitudinal capability has not been detected yet. Start the car once to detect support.")
|
||||
elif toyota_stock_long_forced:
|
||||
availability_note = tr("Factory Toyota longitudinal control is currently enforced. Choose an IQ longitudinal mode to disable it.")
|
||||
elif not alpha_available:
|
||||
availability_note = tr("IQ longitudinal modes are unavailable for this vehicle. Stock ACC is the only available option.")
|
||||
|
||||
self._toggles["LongitudinalControlMode"].set_visible(True)
|
||||
self._long_personality_setting.set_visible(True)
|
||||
|
||||
mode_index = self._get_longitudinal_control_mode_index()
|
||||
longitudinal_control_item = self._toggles["LongitudinalControlMode"]
|
||||
longitudinal_control_item.action_item.set_selected_button(mode_index)
|
||||
longitudinal_control_item.action_item.set_enabled(not ui_state.engaged)
|
||||
longitudinal_control_item.action_item.set_enabled_buttons([True, iq_modes_selectable, iq_modes_selectable, iq_modes_selectable])
|
||||
|
||||
description = tr(DESCRIPTIONS["LongitudinalControlMode"]) + "<br><br>" + e2e_description
|
||||
if availability_note:
|
||||
description += "<br><br><i>" + availability_note + "</i>"
|
||||
longitudinal_control_item.set_description(description)
|
||||
|
||||
personality_enabled = iq_modes_selectable and mode_index in (2, 3)
|
||||
self._long_personality_setting.action_item.set_enabled(personality_enabled)
|
||||
|
||||
# TODO: make a param control list item so we don't need to manage internal state as much here
|
||||
# refresh toggles from params to mirror external changes
|
||||
for param in self._toggle_defs:
|
||||
self._toggles[param].action_item.set_state(self._params.get_bool(param))
|
||||
|
||||
# these toggles need restart, block while engaged
|
||||
for toggle_def in self._toggle_defs:
|
||||
if self._toggle_defs[toggle_def][3] and toggle_def not in self._locked_toggles:
|
||||
self._toggles[toggle_def].action_item.set_enabled(not ui_state.engaged)
|
||||
|
||||
def _render(self, rect):
|
||||
if self._show_iq_dynamic and self._iq_dynamic_panel is not None:
|
||||
self._iq_dynamic_panel.render(rect)
|
||||
else:
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _get_longitudinal_control_mode_index(self) -> int:
|
||||
if not self._params.get_bool("AlphaLongitudinalEnabled"):
|
||||
return 0 # Stock ACC
|
||||
if not self._params.get_bool("ExperimentalMode"):
|
||||
return 1 # IQ.Standard
|
||||
return 2 if self._params.get_bool("IQDynamicMode") else 3 # IQ.Dynamic / IQ.Pilot
|
||||
|
||||
def _apply_longitudinal_control_mode(self, button_index: int):
|
||||
# 0 = Stock ACC, 1 = IQ.Standard, 2 = IQ.Dynamic, 3 = IQ.Pilot
|
||||
previous_alpha = self._params.get_bool("AlphaLongitudinalEnabled")
|
||||
previous_toyota_stock_long = self._params.get_bool("IQToyotaFactoryLong")
|
||||
|
||||
if button_index == 0:
|
||||
self._params.put_bool("AlphaLongitudinalEnabled", False)
|
||||
self._params.put_bool("ExperimentalMode", False)
|
||||
self._params.put_bool("IQDynamicMode", False)
|
||||
elif button_index == 1:
|
||||
self._params.put_bool("AlphaLongitudinalEnabled", True)
|
||||
self._params.put_bool("ExperimentalMode", False)
|
||||
self._params.put_bool("IQDynamicMode", False)
|
||||
self._params.put("LongitudinalPersonality", PERSONALITY_TO_INT["relaxed"])
|
||||
elif button_index == 2:
|
||||
self._params.put_bool("AlphaLongitudinalEnabled", True)
|
||||
self._params.put_bool("ExperimentalMode", True)
|
||||
self._params.put_bool("IQDynamicMode", True)
|
||||
else:
|
||||
self._params.put_bool("AlphaLongitudinalEnabled", True)
|
||||
self._params.put_bool("ExperimentalMode", True)
|
||||
self._params.put_bool("IQDynamicMode", False)
|
||||
|
||||
if button_index != 0 and previous_toyota_stock_long:
|
||||
self._params.put_bool("IQToyotaFactoryLong", False)
|
||||
|
||||
if previous_alpha != self._params.get_bool("AlphaLongitudinalEnabled") or previous_toyota_stock_long != self._params.get_bool("IQToyotaFactoryLong"):
|
||||
self._params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def _toggle_callback(self, state: bool, param: str):
|
||||
self._params.put_bool(param, state)
|
||||
if param == "IQAutoUnits" and state:
|
||||
self._params.remove("IQAutoUnitsRegion")
|
||||
if self._toggle_defs[param][3]:
|
||||
self._params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def _set_longitudinal_personality(self, button_index: int):
|
||||
self._params.put("LongitudinalPersonality", PERSONALITY_DISPLAY_TO_PARAM[button_index])
|
||||
|
||||
def _set_speed_limit_mode(self, button_index: int):
|
||||
self._params.put("IQSpeedAssistMode", button_index)
|
||||
|
||||
def _set_longitudinal_control_mode(self, button_index: int):
|
||||
# 0 = Stock ACC, 1 = IQ.Standard, 2 = IQ.Dynamic, 3 = IQ.Pilot
|
||||
if button_index == self._get_longitudinal_control_mode_index():
|
||||
if button_index == 2 and self._iq_dynamic_panel is not None:
|
||||
self._show_iq_dynamic = True
|
||||
return
|
||||
|
||||
# IQ.Pilot and IQ.Dynamic both require ExperimentalMode confirmation.
|
||||
if button_index in (2, 3) and not self._params.get_bool("ExperimentalModeConfirmed"):
|
||||
def confirm_callback(result: int):
|
||||
if result == DialogResult.CONFIRM:
|
||||
self._apply_longitudinal_control_mode(button_index)
|
||||
self._params.put_bool("ExperimentalModeConfirmed", True)
|
||||
else:
|
||||
self._toggles["LongitudinalControlMode"].action_item.set_selected_button(self._get_longitudinal_control_mode_index())
|
||||
self._update_toggles()
|
||||
|
||||
content = (f"<h1>{self._toggles['LongitudinalControlMode'].title}</h1><br>" +
|
||||
f"<p>{self._toggles['LongitudinalControlMode'].description}</p>")
|
||||
dlg = ConfirmDialog(content, tr("Enable"), rich=True)
|
||||
gui_app.set_modal_overlay(dlg, callback=confirm_callback)
|
||||
else:
|
||||
self._apply_longitudinal_control_mode(button_index)
|
||||
self._update_toggles()
|
||||
Reference in New Issue
Block a user