IQ.Pilot Release Commit @ bec7652

This commit is contained in:
IQ.Lvbs history cleanup
2026-08-22 23:42:41 -05:00
commit 58039e647c
4603 changed files with 1236178 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
"""
from iqpilot.selfdrive.ui.ui_state import ui_state
def restart_needed_callback(_=None):
ui_state.params.put_bool("OnroadCycleRequested", True)

View File

@@ -0,0 +1,155 @@
from iqpilot.common.params import Params
from iqpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
from iqpilot.selfdrive.ui.ui_state import ui_state
from iqpilot.system.hardware.tici.usb_storage import apply_usb_storage_state
from iqpilot.system.ui.widgets import Widget
from iqpilot.system.ui.widgets.list_view import toggle_item
from iqpilot.system.ui.widgets.scroller_tici import Scroller
from iqpilot.system.ui.lib.application import gui_app
from iqpilot.system.ui.lib.multilang import tr, tr_noop
if gui_app.iqpilot_ui():
from iqpilot.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)

View File

@@ -0,0 +1,201 @@
import os
import math
from iqpilot.cereal import messaging, log
from iqpilot.common.basedir import BASEDIR
from iqpilot.common.params import Params
from iqpilot.common.swaglog import cloudlog
from iqpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog
from iqpilot.selfdrive.ui.ui_state import ui_state
from iqpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
from iqpilot.konn3kt.registration import get_cached_dongle_id
from iqpilot.system.hardware import TICI
from iqpilot.system.ui.lib.application import FontWeight, gui_app
from iqpilot.system.ui.lib.multilang import multilang, tr, tr_noop
from iqpilot.system.ui.widgets import Widget, DialogResult
from iqpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
from iqpilot.system.ui.widgets.html_render import HtmlModal
from iqpilot.system.ui.widgets.list_view import text_item, button_item, dual_button_item
from iqpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from iqpilot.system.ui.widgets.scroller_tici import Scroller
if gui_app.iqpilot_ui():
from iqpilot.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."),
}
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
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,
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).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 = self._params.get("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 = self._params.get("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("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, "iqpilot/selfdrive/assets/offroad/fcc.html"))
gui_app.set_modal_overlay(self._fcc_dialog)

View File

@@ -0,0 +1,170 @@
import pyray as rl
from dataclasses import dataclass
from enum import IntEnum
from collections.abc import Callable
from iqpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
from iqpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
from iqpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
from iqpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
from iqpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from iqpilot.system.ui.lib.multilang import tr, tr_noop
from iqpilot.system.ui.lib.text_measure import measure_text_cached
from iqpilot.system.ui.lib.wifi_manager import WifiManager
from iqpilot.system.ui.widgets import Widget
from iqpilot.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()

View File

@@ -0,0 +1,262 @@
import os
import time
import datetime
from iqpilot.common.time_helpers import system_time_valid
from iqpilot.selfdrive.ui.ui_state import ui_state
from iqpilot.system.ui.lib.application import gui_app
from iqpilot.system.ui.lib.multilang import tr, trn
from iqpilot.system.ui.widgets import Widget, DialogResult
from iqpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from iqpilot.system.ui.widgets.list_view import button_item, text_item, ListItem
from iqpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from iqpilot.system.ui.widgets.scroller_tici import Scroller
if gui_app.iqpilot_ui():
from iqpilot.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 iqpilot.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 iqpilot.system.ui.iqwidgets.widgets.list_view import open_text_prompt
from iqpilot.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)

View File

@@ -0,0 +1,343 @@
from iqpilot.cereal import log
from iqpilot.common.params import Params, UnknownKeyName
from iqpilot.system.ui.widgets import Widget
from iqpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item
from iqpilot.system.ui.widgets.scroller_tici import Scroller
from iqpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from iqpilot.system.ui.lib.application import gui_app
from iqpilot.system.ui.lib.multilang import tr, tr_noop
from iqpilot.system.ui.widgets import DialogResult
from iqpilot.selfdrive.ui.ui_state import ui_state
if gui_app.iqpilot_ui():
from iqpilot.system.ui.iqwidgets.widgets.list_view import toggle_item
from iqpilot.system.ui.iqwidgets.widgets.list_view import multiple_button_item
from 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.Chill (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.Chill"), 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.Chill</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.Chill
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.Chill, 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.Chill, 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()