"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
Consolidated IQ.Pilot settings panels (display, visuals, network, maps, steering
and its lane-change / steering-assist sub-panels) in one module.
"""
from iqpilot.cereal import car
from collections.abc import Callable
from enum import Enum
from enum import IntEnum
from iqpilot.common.constants import CV
from iqpilot.common.params import Params
from iqpilot.selfdrive.ui.ui_state import OnroadBrightness
from iqpilot.common.swaglog import cloudlog
from iqpilot.selfdrive.controls.lib.helpers.lane_change import AutoLaneChangeMode
from iqpilot.ui.onroad.offline_tiles import offline_map_root
from iqpilot.selfdrive.ui.layouts.settings.software import time_ago
from iqpilot.selfdrive.ui.ui_state import device, ui_state
from iqpilot.selfdrive.ui.ui_state import ui_state
from iqpilot.system.hardware.hw import Paths
from iqpilot.system.ui.iqwidgets.lib.styles import style
from iqpilot.system.ui.iqwidgets.lib.utils import WideButtonAction
from iqpilot.system.ui.iqwidgets.widgets.list_view import IQListItem, toggle_item
from iqpilot.system.ui.iqwidgets.widgets.list_view import OptionControl
from iqpilot.system.ui.iqwidgets.widgets.list_view import multiple_button_item, toggle_item
from iqpilot.system.ui.iqwidgets.widgets.list_view import option_item, toggle_item, ToggleAction
from iqpilot.system.ui.iqwidgets.widgets.list_view import progress_item
from iqpilot.system.ui.iqwidgets.widgets.list_view import toggle_item, multiple_button_item, IQListItem, IQLineSeparator
from iqpilot.system.ui.iqwidgets.widgets.list_view import toggle_item, option_item, IQLineSeparator
from iqpilot.system.ui.iqwidgets.widgets.list_view import toggle_item, simple_button_item, option_item, IQLineSeparator
from iqpilot.system.ui.iqwidgets.widgets.list_view import PickerGroup, PickerItem, PickerDialog
from iqpilot.system.ui.lib.application import gui_app
from iqpilot.system.ui.lib.multilang import tr
from iqpilot.system.ui.lib.multilang import tr, tr_noop
from iqpilot.system.ui.widgets import DialogResult, Widget
from iqpilot.system.ui.widgets import Widget
from iqpilot.system.ui.widgets.button import Button, ButtonStyle
from iqpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from iqpilot.system.ui.widgets.list_view import text_item
from iqpilot.system.ui.widgets.network import NavButton
from iqpilot.system.ui.widgets.network import NetworkUI, PanelType as NetworkPanelType, TEAL
from iqpilot.system.ui.widgets.scroller_tici import Scroller
from pathlib import Path
from time import monotonic
import datetime
import os
import platform
import pyray as rl
import requests
import shutil
import threading
import time
from iqpilot.cereal import custom
from dataclasses import dataclass
from iqpilot.selfdrive.car.vehicle_catalog import load_catalog
from iqpilot.selfdrive.iqmodeld.models.helpers import select_default_model, is_default_bundle
from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import CUSTOM_MODEL_PATH
from iqpilot.selfdrive.ui.layouts.settings import settings as OP
from iqpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
from iqpilot.system.hardware import HARDWARE
from iqpilot.system.ui.iqwidgets.lib.styles import metrics
from iqpilot.system.ui.iqwidgets.widgets.list_view import (
option_item as option_item,
multiple_button_item as multiple_button_item,
button_item as button_item,
dual_button_item as dual_button_item,
NavSectionButton,
Spacer,
)
from iqpilot.system.ui.iqwidgets.widgets.list_view import IQListItem
from iqpilot.system.ui.iqwidgets.widgets.list_view import IQListItem, IQMultipleButtonAction, IQToggleAction, IQLineSeparator
from iqpilot.system.ui.iqwidgets.widgets.list_view import button_item, toggle_item
from iqpilot.system.ui.iqwidgets.widgets.list_view import NoticeModal
from iqpilot.system.ui.iqwidgets.widgets.list_view import PickerDialog, PickerItem, PickerGroup
from iqpilot.system.ui.lib.application import gui_app, MousePos
from iqpilot.system.ui.lib.multilang import 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 DialogResult
from iqpilot.system.ui.widgets.button import ButtonStyle
from iqpilot.system.ui.widgets.confirm_dialog import alert_dialog, ConfirmDialog
from iqpilot.system.ui.widgets.list_view import button_item
from iqpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from iqpilot.system.ui.widgets.scroller_tici import LineSeparator
from iqpilot.system.ui.widgets.toggle import ON_COLOR
import re
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, time_ago
from functools import partial
from iqdbc.car.hyundai.values import CAR, CANFD_UNSUPPORTED_LONGITUDINAL_CAR, UNSUPPORTED_LONGITUDINAL_CAR
from iqdbc.car.subaru.values import CAR, SubaruFlags
from iqdbc.car.volkswagen.values import CAR, VolkswagenFlags
from iqpilot.common.basedir import BASEDIR
from iqpilot.system.ui.iqwidgets.lib.styles import ink
from iqpilot.system.ui.iqwidgets.widgets.list_view import multiple_button_item
from iqpilot.system.ui.iqwidgets.widgets.list_view import toggle_item
from iqpilot.system.ui.lib.application import gui_app, FontWeight
from iqpilot.system.ui.widgets.list_view import ButtonAction
import json
ONROAD_BRIGHTNESS_TIMER_VALUES = {0: 15, 1: 30, **{i: (i - 1) * 60 for i in range(2, 12)}}
def _fmt_seconds(value) -> str:
return f"{value} s" if value < 60 else f"{int(value / 60)} m"
def onroad_brightness_label(val) -> str:
if val == OnroadBrightness.AUTO:
return tr("Auto")
if val == OnroadBrightness.AUTO_DARK:
return tr("Auto — Dim")
return f"{(val - 1) * 5} %"
class DisplayLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
self._scroller = Scroller(self._build_rows(), line_separator=True, spacing=0)
def _build_rows(self):
self._force_mici_ui = toggle_item(
title=lambda: tr("Force Mici UI"),
description=lambda: tr("Run the compact comma 4 (mici) interface on this device") if self._params.get_bool("ForceSmallUI") else "",
param="ForceSmallUI",
)
self._brightness_row = option_item(
param="Brightness", title=lambda: tr("Display Brightness"), description="",
min_value=0, max_value=100, value_change_step=5,
label_callback=lambda v: tr("Default") if v == 0 else f"{v} %", inline=True,
)
self._onroad_brightness_row = option_item(
param="OnroadScreenOffBrightness", title=lambda: tr("Driving Brightness"), description="",
min_value=0, max_value=21, value_change_step=1,
label_callback=onroad_brightness_label, inline=True,
)
self._dim_delay_row = option_item(
param="OnroadScreenOffTimer", title=lambda: tr("Dim Delay"), description="",
min_value=0, max_value=11, value_change_step=1,
value_map=ONROAD_BRIGHTNESS_TIMER_VALUES, label_callback=_fmt_seconds, inline=True,
)
self._idle_close_row = option_item(
param="InteractivityTimeout", title=lambda: tr("Auto-Close Settings"),
description=lambda: tr("How long the settings screen may sit untouched before it closes itself."),
min_value=0, max_value=120, value_change_step=10,
label_callback=lambda v: tr("Default") if not v else _fmt_seconds(v), inline=True,
)
return [
self._force_mici_ui,
self._brightness_row,
self._onroad_brightness_row,
self._dim_delay_row,
self._idle_close_row,
]
update_onroad_brightness = staticmethod(onroad_brightness_label)
def _update_state(self):
super()._update_state()
for row in self._scroller._items:
action = row.action_item
if isinstance(action, ToggleAction) and action.toggle.param_key is not None:
action.set_state(self._params.get_bool(action.toggle.param_key))
elif isinstance(action, OptionControl) and action.param_key is not None:
action.set_value(self._params.get(action.param_key, return_default=True))
mode = self._params.get("OnroadScreenOffBrightness", return_default=True)
self._dim_delay_row.action_item.set_enabled(mode not in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK))
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()
CHEVRON_INFO_DESCRIPTION = {
"enabled": tr_noop("Pins live readouts (gap, speed, time-to-lead) to the chevron marking the car ahead. "
"Needs IQ.Pilot to be the one controlling gas and brake."),
"disabled": tr_noop("Unavailable — IQ.Pilot longitudinal control is not active on this car."),
}
_HUD_TOGGLES = {
"IQBlindSpotAlerts": (
lambda: tr("Blind Spot Alerts"),
lambda: tr("Flashes a side warning whenever the car reports something sitting in your blind spot (BSM-equipped cars only)."),
),
"IQExpandedStatus": (
lambda: tr("Expanded Status Bar"),
lambda: tr("Bring back the classic UI's wide offroad status strip: temperature, vehicle, and Konn3kt state at a glance."),
),
"IQSteerEffortArc": (
lambda: tr("Steering Effort Arc"),
lambda: tr("Trace an arc over the road view showing how much steering IQ.Pilot is applying while lateral control runs."),
),
"IQRoadNameOverlay": (
lambda: tr("Road Name Overlay"),
lambda: tr("Show the current road's name over the driving view."
"
Requires offline map data for your region to be installed."),
),
"IQBlinkerIndicators": (
lambda: tr("Blinker Indicators"),
lambda: tr("Mirror the car's blinkers as arrows on the driving screen."),
),
"IQAccelMeter": (
lambda: tr("Acceleration Meter"),
lambda: tr("Draw a bar along the left edge tracking measured acceleration and braking — what the car is actually "
"doing right now, not the planner's request."),
),
}
_SECTION_TITLE_COLOR = rl.Color(16, 185, 169, 255)
class VisualsLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
self._display_layout = DisplayLayout()
self._scroller = Scroller(self._build_rows(), line_separator=True, spacing=0)
def _build_rows(self):
self._toggles = {
key: toggle_item(title=title, description=desc, param=key,
initial_state=ui_state.params.get_bool(key))
for key, (title, desc) in _HUD_TOGGLES.items()
}
self._chevron_info = multiple_button_item(
title=lambda: tr("Lead Vehicle Readouts"),
description="",
buttons=[lambda: tr("Off"), lambda: tr("Distance"), lambda: tr("Speed"), lambda: tr("Time"), lambda: tr("All")],
param="IQLeadReadouts",
inline=False,
)
self._dev_ui_info = toggle_item(
title=lambda: tr("Onroad Developer UI"),
description=lambda: tr("Overlay a bar of live control metrics (steering, lateral accel, lead data) while driving."),
initial_state=bool(int(ui_state.params.get("IQDevUIInfo", return_default=True))),
callback=lambda on: ui_state.params.put("IQDevUIInfo", 3 if on else 0),
)
display_rows = self._display_layout._scroller._items[1:]
return [
IQListItem(title=lambda: tr("Display"), description="", action_item=None, inline=True,
title_color=_SECTION_TITLE_COLOR),
IQLineSeparator(20),
*display_rows,
IQLineSeparator(60),
*self._toggles.values(),
self._chevron_info,
self._dev_ui_info,
]
def _sync_chevron_row(self):
if ui_state.has_longitudinal_control:
self._chevron_info.set_description(tr(CHEVRON_INFO_DESCRIPTION["enabled"]))
self._chevron_info.action_item.set_selected_button(ui_state.params.get("IQLeadReadouts", return_default=True))
self._chevron_info.action_item.set_enabled(True)
else:
self._chevron_info.set_description(tr(CHEVRON_INFO_DESCRIPTION["disabled"]))
self._chevron_info.action_item.set_enabled(False)
ui_state.params.put("IQLeadReadouts", 0)
def _update_state(self):
super()._update_state()
for key, row in self._toggles.items():
row.action_item.set_state(self._params.get_bool(key))
self._dev_ui_info.action_item.set_state(bool(int(ui_state.params.get("IQDevUIInfo", return_default=True))))
self._sync_chevron_row()
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._display_layout.show_event()
self._scroller.show_event()
if not ui_state.has_longitudinal_control:
self._chevron_info.set_description(tr(CHEVRON_INFO_DESCRIPTION["disabled"]))
self._chevron_info.show_description(True)
_IDLE_BG = rl.Color(52, 55, 62, 255)
class ScanPhase(Enum):
IDLE = 0
RUNNING = 1
FAILED = 2
class IQNetworkUI(NetworkUI):
def __init__(self, wifi_manager):
super().__init__(wifi_manager)
self._phase = ScanPhase.IDLE
self.scan_button = Button(lambda: tr("Searching...") if self._phase == ScanPhase.RUNNING else tr("Scan"), self._start_scan,
button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT,
font_size=60, border_radius=30)
self.scan_button.set_rect(rl.Rectangle(0, 0, 400, 100))
self._wifi_manager.add_callbacks(networks_updated=self._on_networks_updated)
def _set_phase(self, phase: ScanPhase):
self._phase = phase
running = phase == ScanPhase.RUNNING
self.scan_button.set_enabled(not running)
self._wifi_manager._scanning = running
def _start_scan(self):
self._set_phase(ScanPhase.RUNNING)
threading.Thread(target=self._scan_worker, daemon=True).start()
def _scan_worker(self):
try:
self._wifi_manager._update_networks()
self._wifi_manager._request_scan()
self._wifi_manager._last_network_update = time.monotonic()
except Exception:
cloudlog.exception("IQNetworkUI scan failed")
self._phase = ScanPhase.FAILED
def _on_networks_updated(self, networks):
if self._phase == ScanPhase.RUNNING:
self._set_phase(ScanPhase.IDLE)
def _render(self, rect: rl.Rectangle):
super()._render(rect)
if self._phase == ScanPhase.FAILED:
self._set_phase(ScanPhase.IDLE)
if self._current_panel == NetworkPanelType.WIFI:
self.scan_button.set_position(self._rect.x, self._rect.y + 20)
r = self.scan_button.rect
bg = TEAL if self.scan_button.enabled else _IDLE_BG
rl.draw_rectangle_rounded(r, 30 / (min(r.width, r.height) / 2), 10, bg)
self.scan_button.render()
MAP_PATH = Path(Paths.mapd_root()) / "offline"
OFFLINE_TILES_PATH = offline_map_root() / "regions"
_BOUNDS_BASE_URL = "https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/"
_REGION_PARAMS = ("OsmDownloadedDate", "OsmLocal", "OsmLocationName", "OsmLocationTitle", "OsmStateName", "OsmStateTitle")
def _fmt_bytes(n: int) -> str:
return f"{n / 1024 ** 2:.2f} MB" if n < 1024 ** 3 else f"{n / 1024 ** 3:.2f} GB"
def _tree_size(*roots: Path) -> int:
total = 0
pending = [p for p in roots if p.exists()]
while pending:
try:
for entry in os.scandir(pending.pop()):
if entry.is_file():
total += entry.stat().st_size
elif entry.is_dir():
pending.append(entry.path)
except OSError:
pass
return total
class IQMapsLayout(Widget):
def __init__(self):
super().__init__()
self._current_percent = 0
self._last_sync = 0
self._mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else ui_state.params
self._build_rows()
self._refresh_disk_usage()
self._progress.set_visible(False)
self._tile_progress.set_visible(False)
self._state_btn.set_visible(False)
self._mapd_version.action_item.set_text(ui_state.params.get("MapdVersion") or "Loading...")
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
def _build_rows(self):
self._mapd_version = text_item(tr("Routing Engine Version"), lambda: ui_state.params.get("MapdVersion") or "Loading...")
self._online_maps_toggle = toggle_item(
tr("Online On-Screen Maps"),
tr("Pull live Mapbox tiles for the on-screen map whenever the device has internet."),
param="OnlineOSMaps",
)
self._offline_maps_toggle = toggle_item(
tr("Offline On-Screen Maps"),
tr("Draw the on-screen map from tiles stored on the device. Combined with Online, stored tiles "
"take over the moment connectivity drops. Tiles come down with your selected region."),
param="OfflineOSMaps",
)
self._delete_maps_btn = IQListItem(tr("Stored Maps"), action_item=WideButtonAction(tr("DELETE"), enabled=True),
callback=self._confirm_wipe)
self._progress = progress_item(tr("Fetching Map"))
self._tile_progress = progress_item(tr("Downloading Map Tiles"))
self._update_btn = IQListItem(tr("Map Database"), action_item=WideButtonAction(tr("CHECK"), enabled=True),
callback=self._confirm_db_refresh)
self._country_btn = IQListItem(tr("Country"), action_item=WideButtonAction(tr("SELECT"), enabled=True),
callback=lambda: self._open_region_picker("Country"))
self._state_btn = IQListItem(tr("State"), action_item=WideButtonAction(tr("SELECT"), enabled=True),
callback=lambda: self._open_region_picker("State"))
self.items = [self._mapd_version, self._online_maps_toggle, self._offline_maps_toggle, self._delete_maps_btn,
self._progress, self._tile_progress, self._update_btn, self._country_btn, self._state_btn]
def _refresh_disk_usage(self):
def worker():
self._delete_maps_btn.action_item.set_value(_fmt_bytes(_tree_size(MAP_PATH, OFFLINE_TILES_PATH)))
threading.Thread(target=worker, daemon=True).start()
def _confirm_wipe(self):
self._ask(tr("This will delete ALL downloaded maps\n\nAre you sure you want to delete all maps?"),
tr("Yes, delete all maps"), self._start_wipe)
def _start_wipe(self):
self._delete_maps_btn.action_item.set_enabled(False)
self._delete_maps_btn.action_item.set_text(tr("REMOVING..."))
threading.Thread(target=self._wipe_worker, daemon=True).start()
def _wipe_worker(self):
for root in (MAP_PATH, OFFLINE_TILES_PATH):
if root.exists():
shutil.rmtree(root)
for param in _REGION_PARAMS:
ui_state.params.remove(param)
self._delete_maps_btn.action_item.set_enabled(True)
self._delete_maps_btn.action_item.set_text(tr("DELETE"))
self._refresh_disk_usage()
@staticmethod
def _ask(msg, confirm_text, then):
gui_app.set_modal_overlay(ConfirmDialog(msg, confirm_text),
lambda res: then() if res == DialogResult.CONFIRM else None)
def _confirm_db_refresh(self):
self._ask(tr("This will start the download process and it might take a while to complete."),
tr("Start Download"), self._request_db_download)
def _request_db_download(self):
ui_state.params.put_bool("OsmDbUpdatesCheck", True)
nations = [c] if (c := ui_state.params.get("OsmLocationName") or "") else []
states = [s] if (s := ui_state.params.get("OsmStateName") or "") else []
if "US" in nations and states:
if any(x.lower() == "all" for x in states):
states = [x for x in states if x.lower() != "all"]
else:
nations.remove("US")
self._mem_params.put("OSMDownloadLocations", {"nations": nations, "states": states})
def _open_region_picker(self, region_type):
btn = self._country_btn if region_type == "Country" else self._state_btn
btn.action_item.set_enabled(False)
btn.action_item.set_text(tr("LOADING..."))
threading.Thread(target=self._region_picker_worker, args=(region_type, btn), daemon=True).start()
@staticmethod
def _fetch_regions(region_type):
url = _BOUNDS_BASE_URL + ("nation_bounding_boxes.json" if region_type == "Country" else "us_states_bounding_boxes.json")
try:
data = requests.get(url, timeout=10).json()
return sorted((PickerItem(ref=k, data={'display_name': v['full_name']}) for k, v in data.items()),
key=lambda n: n.data['display_name'])
except Exception:
return []
def _region_picker_worker(self, region_type, btn):
locations = self._fetch_regions(region_type)
if region_type == "State":
locations.insert(0, PickerItem(ref="All", data={'display_name': tr("All states (~6.0 GB)")}))
btn.action_item.set_enabled(True)
btn.action_item.set_text(tr("SELECT"))
key = "OsmLocation" if region_type == "Country" else "OsmState"
current = ui_state.params.get(f"{key}Name") or ""
dialog = PickerDialog(tr(f"Select {region_type}"), [PickerGroup(folder="", nodes=locations)],
current_ref=current, search_prompt=tr("Perform a search"))
done = lambda res: self._on_region_picked(region_type, locations, key, res, dialog.selection_ref) # noqa: E731
dialog.on_exit = done
gui_app.set_modal_overlay(dialog, callback=done)
def _on_region_picked(self, region_type, locations, key, res, ref):
if res != DialogResult.CONFIRM or not ref:
if region_type == "State" and res == DialogResult.CANCEL:
if ui_state.params.get("OsmLocationName") == "US" and not ui_state.params.get("OsmStateName"):
for param in ("OsmLocationName", "OsmLocationTitle", "OsmLocal"):
ui_state.params.remove(param)
self._sync_rows()
return
if region_type == "Country":
ui_state.params.put_bool("OsmLocal", True)
ui_state.params.remove("OsmStateName")
ui_state.params.remove("OsmStateTitle")
ui_state.params.put(f"{key}Name", ref)
title = next((n.data['display_name'] for n in locations if n.ref == ref), ref)
ui_state.params.put(f"{key}Title", title)
if ref == "US" and region_type == "Country":
self._open_region_picker("State")
else:
self._confirm_db_refresh()
def _sync_tile_progress(self):
if not self._mem_params.get("OfflineTilesDownloadRequest"):
self._tile_progress.set_visible(False)
return
device._reset_interactive_timeout()
self._refresh_disk_usage()
self._tile_progress.set_visible(True)
progress = self._mem_params.get("OfflineTilesDownloadProgress") or {}
total = progress.get("total_bytes", 0)
done = progress.get("downloaded_bytes", 0)
if total > 0:
pct = max(0.0, min(100.0, done / total * 100.0))
self._tile_progress.action_item.update(pct, f"{int(pct)}% - Downloading Map Tiles", show_progress=True)
else:
self._tile_progress.action_item.update(0.0, tr("Preparing Map Tiles..."), show_progress=False)
def _sync_db_progress(self, downloading: bool):
self._progress.set_visible(True)
progress = ui_state.params.get("OSMDownloadProgress") or {}
total = progress.get('total_files', 0)
done = progress.get('downloaded_files', 0)
failed = total > 0 and not downloading and done < total
if failed:
self._current_percent = 0.0
bar_text, btn_text = "0% - Downloading Maps", tr("Download failed — try again.")
elif total > 0 and downloading:
self._current_percent = max(0.0, min(100.0, done / total * 100.0))
pct = int(self._current_percent)
bar_text, btn_text = f"{pct}% - Downloading Maps", f"{done}/{total} ({pct}%)"
else:
self._current_percent = 0.0
bar_text, btn_text = "0% - Downloading Maps", tr("Fetching Maps...")
self._progress.action_item.update(self._current_percent, bar_text,
show_progress=total > 0 and downloading and not failed)
self._update_btn.action_item.set_value(btn_text)
def _set_rows_busy(self, busy: bool):
for row in (self._update_btn, self._country_btn, self._state_btn, self._delete_maps_btn):
row.action_item.set_enabled(not busy)
def _sync_rows(self):
downloading = bool(self._mem_params.get("OSMDownloadLocations"))
self._sync_tile_progress()
self._country_btn.set_enabled(not downloading)
self._state_btn.set_enabled(not downloading)
self._state_btn.set_visible(ui_state.params.get("OsmLocationName") == "US")
self._update_btn.set_visible(bool(ui_state.params.get("OsmLocationName")))
self._country_btn.action_item.set_value(ui_state.params.get("OsmLocationTitle") or "")
self._state_btn.action_item.set_value(ui_state.params.get("OsmStateTitle") or "")
if downloading or ui_state.params.get_bool("OsmDbUpdatesCheck"):
if downloading:
device._reset_interactive_timeout()
self._refresh_disk_usage()
self._sync_db_progress(downloading)
self._set_rows_busy(downloading) # TODO-IQ: introduce CANCEL database download with mapd
return
self._progress.set_visible(False)
self._set_rows_busy(False)
dt = None
if ts := ui_state.params.get("OsmDownloadedDate"):
try:
if float(ts) > 0:
dt = datetime.datetime.fromtimestamp(float(ts), tz=datetime.UTC)
except (ValueError, TypeError):
dt = None
self._update_btn.action_item.set_value(tr("Checked {}").format(time_ago(dt)))
def _update_state(self):
now = monotonic()
if now - self._last_sync >= 1.0:
self._last_sync = now
self._sync_rows()
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()
_TIMER_LABELS = {
-1: lambda: tr("Off"),
0: lambda: tr("Nudge"),
1: lambda: tr("No Nudge"),
2: lambda: f"0.5 {tr('s')}",
3: lambda: f"1 {tr('s')}",
4: lambda: f"2 {tr('s')}",
5: lambda: f"3 {tr('s')}",
}
class LaneChangeSettingsLayout(Widget):
def __init__(self, back_btn_callback: Callable):
super().__init__()
self._back_button = NavButton(tr("Back"))
self._back_button.set_click_callback(back_btn_callback)
self._scroller = Scroller(self._build_rows(), line_separator=False, spacing=0)
def _build_rows(self):
self._lane_change_timer = option_item(
title=lambda: tr("Blinker-Initiated Lane Change"),
param="IQLaneChangeTimer",
description=lambda: tr("Delay before a blinker-triggered lane change starts on its own — no wheel nudge "
"needed once a delay is set (default is Nudge).
Use the blinker for this only "
"when traffic and the road actually allow the maneuver."),
min_value=-1, max_value=5, value_change_step=1,
label_callback=lambda x: _TIMER_LABELS[int(x)](),
)
self._bsm_delay = toggle_item(
param="IQLaneChangeBsmDelay",
title=lambda: tr("Hold Lane Change for Blind Spot"),
description=lambda: tr("Hold the automatic lane change while blind spot monitoring reports a car in the "
"target lane, releasing it once the lane is clear."),
)
self._edge_guard = toggle_item(
param="IQEdgeGuard",
title=lambda: tr("Lane Edge Guard"),
description=lambda: tr("Blocks lane changes when a road edge is detected on the target side."),
)
self._continuous = toggle_item(
param="LaneChangeContinuous",
title=lambda: tr("Auto Lane Change: Continuous Changes"),
description=lambda: tr("Normally one maneuver ends the lane change even with the blinker held. Turn this on "
"to chain further changes while the blinker stays on — each follow-up still wants a "
"wheel nudge."),
)
return [
self._lane_change_timer,
IQLineSeparator(40),
self._bsm_delay,
IQLineSeparator(40),
self._edge_guard,
IQLineSeparator(40),
self._continuous,
]
def _update_state(self):
super()._update_state()
has_bsm = bool(ui_state.CP and ui_state.CP.enableBsm)
if not has_bsm and ui_state.params.get_bool("IQLaneChangeBsmDelay"):
ui_state.params.remove("IQLaneChangeBsmDelay")
timer_armed = ui_state.params.get("IQLaneChangeTimer", return_default=True) > AutoLaneChangeMode.NUDGE
self._bsm_delay.action_item.set_enabled(has_bsm and timer_armed)
def _render(self, rect):
self._back_button.set_position(self._rect.x, self._rect.y + 20)
self._back_button.render()
below_button = self._back_button.rect.height + 40
self._scroller.render(rl.Rectangle(rect.x, rect.y + below_button, rect.width, rect.height - below_button))
def show_event(self):
self._scroller.show_event()
SAB_BRAKE_RESPONSE_OPTIONS = [
(tr_noop("Stay Engaged"), tr_noop("Remain Active: braking never interrupts steering assistance.")),
(tr_noop("Standby"), tr_noop("Standby: braking parks steering assistance; it rejoins once you're off the pedal.")),
(tr_noop("Disengage"), tr_noop("Disengage: braking shuts steering assistance off entirely.")),
]
_MODE_DISENGAGE = 2
_MODE_STANDBY = 1
SAB_AVAILABILITY_DESC = tr("Note: on cars lacking an LFA/LKAS button, turning this off keeps steering assistance "
"from arming as cruise availability flips.")
SAB_DRIVER_INTERVENTION_DESC = tr("Pick what a brake-pedal press does to steering assistance.")
SAB_STEERING_OVERRIDE_DESC = tr(
"Steering assistance stands by while you steer yourself and rejoins the moment you let go. It stays armed the whole time; only the torque stops."
)
DEFAULT_TO_OFF = tr("Locked OFF on this platform; the vehicle can't support other choices.")
STATUS_DISENGAGE_ONLY = tr("This platform is limited to Disengage mode.")
class SabSettingsLayout(Widget):
def __init__(self, back_btn_callback: Callable):
super().__init__()
self._back_button = NavButton(tr("Back"))
self._back_button.set_click_callback(back_btn_callback)
self._scroller = Scroller(self._build_rows(), line_separator=True, spacing=0)
def _build_rows(self):
self._main_cruise_toggle = toggle_item(
title=lambda: tr("Availability While Cruise Changes"),
description=SAB_AVAILABILITY_DESC,
param="AolMainCruiseAllowed",
)
self._disengage_on_brake_toggle = toggle_item(
title=lambda: tr("Driver Intervention Handling"),
description=SAB_DRIVER_INTERVENTION_DESC,
initial_state=False,
callback=self._on_brake_shortcut_toggled,
)
self._steering_mode = multiple_button_item(
param="AolSteeringMode",
title=lambda: tr("Brake Response Mode"),
description="",
buttons=[lambda label=label: tr(label) for label, _ in SAB_BRAKE_RESPONSE_OPTIONS],
inline=False,
button_width=350,
callback=self._refresh_mode_description,
)
self._pause_on_steering_override_toggle = toggle_item(
title=lambda: tr("Pause While You Steer"),
description=SAB_STEERING_OVERRIDE_DESC,
param="AolPauseOnSteeringOverride",
)
return [self._main_cruise_toggle, self._disengage_on_brake_toggle, self._steering_mode,
self._pause_on_steering_override_toggle]
@staticmethod
def _has_limited_sab_options() -> bool:
brand = ""
if ui_state.is_offroad():
if bundle := ui_state.params.get("CarPlatformBundle"):
brand = bundle.get("brand", "")
if not brand and ui_state.CP:
brand = ui_state.CP.brand
return brand == "rivian"
def _on_brake_shortcut_toggled(self, enabled: bool):
current = int(ui_state.params.get("AolSteeringMode", return_default=True))
if enabled:
target = _MODE_DISENGAGE
elif current == _MODE_DISENGAGE:
target = _MODE_STANDBY
else:
return
ui_state.params.put("AolSteeringMode", target)
self._steering_mode.action_item.set_selected_button(target)
def _refresh_mode_description(self, selected: int):
lines = [tr("Pick how steering assistance reacts once the driver brakes in IQ.Pilot."), ""]
for i, (_, blurb) in enumerate(SAB_BRAKE_RESPONSE_OPTIONS):
text = tr(blurb)
lines.append(f"{text}" if i == selected else text)
self._steering_mode.set_description("
".join(lines))
self._steering_mode.show_description(True)
def _apply_limited_platform(self):
ui_state.params.remove("AolMainCruiseAllowed")
ui_state.params.put_bool("AolUnifiedEngagementMode", True)
ui_state.params.put("AolSteeringMode", _MODE_DISENGAGE)
self._main_cruise_toggle.action_item.set_enabled(False)
self._main_cruise_toggle.action_item.set_state(False)
self._main_cruise_toggle.set_description(f"{DEFAULT_TO_OFF}
{SAB_AVAILABILITY_DESC}")
self._disengage_on_brake_toggle.action_item.set_enabled(False)
self._disengage_on_brake_toggle.action_item.set_state(True)
self._disengage_on_brake_toggle.set_description(STATUS_DISENGAGE_ONLY)
self._steering_mode.action_item.set_enabled(False)
self._steering_mode.set_description(STATUS_DISENGAGE_ONLY)
self._steering_mode.action_item.set_selected_button(_MODE_DISENGAGE)
def _apply_full_platform(self):
self._main_cruise_toggle.action_item.set_enabled(True)
self._main_cruise_toggle.set_description(SAB_AVAILABILITY_DESC)
self._disengage_on_brake_toggle.action_item.set_enabled(True)
self._disengage_on_brake_toggle.set_description(SAB_DRIVER_INTERVENTION_DESC)
self._steering_mode.action_item.set_enabled(True)
def _update_state(self):
super()._update_state()
if ui_state.params.get_bool("AolEnabled"):
ui_state.params.put_bool("AolUnifiedEngagementMode", True)
current = int(ui_state.params.get("AolSteeringMode", return_default=True))
self._disengage_on_brake_toggle.action_item.set_state(current == _MODE_DISENGAGE)
self._refresh_mode_description(self._steering_mode.action_item.get_selected_button())
if self._has_limited_sab_options():
self._apply_limited_platform()
else:
self._apply_full_platform()
def _render(self, rect):
self._back_button.set_position(self._rect.x, self._rect.y + 20)
self._back_button.render()
below_button = self._back_button.rect.height + 40
self._scroller.render(rl.Rectangle(rect.x, rect.y + below_button, rect.width, rect.height - below_button))
def show_event(self):
self._scroller.show_event()
class PanelType(IntEnum):
STEERING = 0
SAB = 1
LANE_CHANGE = 2
class SteeringLayout(Widget):
def __init__(self):
super().__init__()
self._current_panel = PanelType.STEERING
self._lane_change_settings_layout = LaneChangeSettingsLayout(lambda: self._set_current_panel(PanelType.STEERING))
self._sab_settings_layout = SabSettingsLayout(lambda: self._set_current_panel(PanelType.STEERING))
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=False, spacing=0)
for ctrl, key in [(self._lane_turn_value_control, "IQLaneTurnValue"), (self._delay_control, "IQSoftwareSteerDelay")]:
ctrl.action_item.set_value(int(float(ui_state.params.get(key, return_default=True)) * 100))
def _initialize_items(self):
self._aol_base_desc = tr("Enable Always on Lateral (AOL). Disable this toggle to return to stock IQ.Pilot steering engagement behavior.")
self._sab_limited_desc = tr("This platform supports a limited set of steering assistance behavior options.")
self._sab_full_desc = tr("This platform supports the full steering assistance behavior configuration.")
self._sab_check_compat_desc = tr("Start the vehicle to check steering assistance behavior compatibility.")
self._aol_toggle = toggle_item(
param="AolEnabled",
title=lambda: tr("Always on Lateral (AOL)"),
description=self._aol_base_desc,
)
self._aol_settings_button = simple_button_item(
button_text=lambda: tr("Steering Assistance Behavior"),
button_width=800,
callback=lambda: self._set_current_panel(PanelType.SAB)
)
self._lane_change_settings_button = simple_button_item(
button_text=lambda: tr("Lane Change Options"),
button_width=800,
callback=lambda: self._set_current_panel(PanelType.LANE_CHANGE)
)
self._nnff_toggle = toggle_item(
param="NeuralNetworkFeedForward",
title=lambda: tr("Neural Network Feed Forward (NNFF)"),
description=""
)
self._lane_turn_desire_toggle = toggle_item(
lambda: tr("Low-Speed Turn Planning"),
lambda: tr("If you're driving at 20 mph (32 km/h) or below and have your blinker on,"
" the car will plan a turn in that direction at the nearest drivable path."
" This prevents situations (like at red lights) where the car might plan the wrong turn direction."),
param="IQLaneTurnDesire"
)
self._lane_turn_value_control = option_item(
tr("Adjust Lane Turn Desire Speed"), "IQLaneTurnValue", 500, 2000,
tr("Sets the speed threshold used by Lane Turn Desires. Below this value, turn desires can plan into the nearest drivable turn path. Default is 19 mph."),
int(round(100 / CV.MPH_TO_KPH)), None, True, "", style.BUTTON_ACTION_WIDTH, None, True,
lambda v: f"{int(round(v * (CV.MPH_TO_KPH if ui_state.is_metric else 1)))}"
f" {'km/h' if ui_state.is_metric else 'mph'}"
)
self._steer_delay_toggle = toggle_item(lambda: tr("Self-Tuning Steer Delay"), "", param="IQLiveSteerDelay")
self._delay_control = option_item(
tr("Manual Delay Offset"), "IQSoftwareSteerDelay", 5, 50,
tr("How much lead time to add on top of the car's own steering rack delay while self-tuning is off. Default is 0.2 s."),
1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True, lambda v: f"{float(v):.2f}s"
)
items = [
self._aol_toggle,
self._nnff_toggle,
IQLineSeparator(40),
self._aol_settings_button,
IQLineSeparator(40),
self._lane_change_settings_button,
IQLineSeparator(40),
self._lane_turn_desire_toggle,
self._lane_turn_value_control,
IQLineSeparator(40),
self._steer_delay_toggle,
self._delay_control,
]
return items
def _set_current_panel(self, panel: PanelType):
self._current_panel = panel
def _update_state(self):
super()._update_state()
steering_supported = True
if ui_state.CP is not None:
sab_desc = self._sab_limited_desc if self._sab_settings_layout._has_limited_sab_options() else self._sab_full_desc
self._aol_toggle.set_description(f"{sab_desc}
{self._aol_base_desc}")
if ui_state.CP.steerControlType == car.CarParams.SteerControlType.angle:
ui_state.params.remove("NeuralNetworkFeedForward")
steering_supported = False
else:
self._aol_toggle.set_description(f"{self._sab_check_compat_desc}
{self._aol_base_desc}")
ui_state.params.remove("NeuralNetworkFeedForward")
steering_supported = False
self._aol_toggle.action_item.set_enabled(ui_state.is_offroad())
self._aol_settings_button.action_item.set_enabled(ui_state.is_offroad() and self._aol_toggle.action_item.get_state())
self._nnff_toggle.action_item.set_enabled(ui_state.is_offroad() and steering_supported)
turn_desire = ui_state.params.get_bool("IQLaneTurnDesire")
live_delay = ui_state.params.get_bool("IQLiveSteerDelay")
self._lane_turn_desire_toggle.action_item.set_state(turn_desire)
self._lane_turn_value_control.set_visible(turn_desire)
self._steer_delay_toggle.action_item.set_state(live_delay)
self._delay_control.set_visible(not live_delay)
new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100
if self._lane_turn_value_control.action_item.value_change_step != new_step:
self._lane_turn_value_control.action_item.value_change_step = new_step
delay_desc = tr("Let IQ.Pilot measure how long your steering takes to respond and keep that figure up to date. "
"Switch it off to pin the timing yourself.")
if live_delay:
delay_desc += f"
{tr('Measured:')} {ui_state.sm['lateralDelay'].lateralDelay:.3f} s"
elif ui_state.CP:
sw = float(ui_state.params.get("IQSoftwareSteerDelay", "0.2"))
cp = ui_state.CP.steerActuatorDelay
delay_desc += f"
{tr('Rack:')} {cp:.2f} s + {tr('Offset:')} {sw:.2f} s = {tr('Total:')} {cp + sw:.2f} s"
self._steer_delay_toggle.set_description(delay_desc)
def _render(self, rect):
if self._current_panel == PanelType.LANE_CHANGE:
self._lane_change_settings_layout.render(rect)
elif self._current_panel == PanelType.SAB:
self._sab_settings_layout.render(rect)
else:
self._scroller.render(rect)
def show_event(self):
self._set_current_panel(PanelType.STEERING)
self._scroller.show_event()
class CruiseLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
self._scroller = Scroller(self._initialize_items(), line_separator=True, spacing=0)
@staticmethod
def _toggle_item(title: str, description: str, param: str):
return IQListItem(
title=lambda: tr(title),
description=lambda: tr(description),
action_item=IQToggleAction(initial_state=Params().get_bool(param), callback=lambda state: Params().put_bool(param, state), param=param),
)
@staticmethod
def _mode_item(title: str, description: str, param: str, labels: list[str], width: int, inline: bool = False):
return IQListItem(
title=lambda: tr(title),
description=lambda: tr(description),
action_item=IQMultipleButtonAction(buttons=[lambda s=s: tr(s) for s in labels], button_width=width, param=param),
inline=inline,
)
@staticmethod
def _option_item(title: str, description: str, param: str, minimum: int, maximum: int, step: int = 1,
label_callback=None, use_float_scaling: bool = False):
return IQListItem(
title=lambda: tr(title),
description=lambda: tr(description),
action_item=OptionControl(
param=param,
min_value=minimum,
max_value=maximum,
value_change_step=step,
use_float_scaling=use_float_scaling,
label_callback=label_callback,
),
)
def _initialize_items(self):
ms_to_mph = 2.23694
def speed_label(value: float | int) -> str:
return f"{int(round(value * ms_to_mph))} mph"
def seconds_label(value: float | int) -> str:
return f"{float(value):.1f}s"
def distance_label(value: float | int) -> str:
value = int(value)
return "Stock" if value == 0 else f"{value:+d} m"
items = [
IQListItem(title=lambda: tr("Speed Limit Control"), description="", action_item=None, inline=True,
title_color=rl.Color(16, 185, 169, 255)),
IQLineSeparator(20),
self._mode_item(
"IQ Speed Limit Mode",
"Choose how IQ.Pilot handles speed limit data. Control adjusts cruise speed. Warning only highlights overspeed.",
"IQSpeedAssistMode",
["Off", "Info", "Warn", "Control"],
180,
),
self._mode_item(
"IQ SLC Policy",
"Select how IQ.Pilot resolves conflicting speed limit sources.",
"SLCPolicy",
["Map", "Priority", "Combined"],
180,
),
self._toggle_item(
"IQ SLC Confirm Higher",
"Require confirmation before IQ.Pilot accepts a higher detected speed limit.",
"SpeedLimitConfirmationHigher",
),
self._toggle_item(
"IQ SLC Confirm Lower",
"Require confirmation before IQ.Pilot accepts a lower detected speed limit.",
"SpeedLimitConfirmationLower",
),
self._toggle_item(
"IQ SLC Auto Confirm",
"Automatically accept speed limit changes after a 5-second timeout without requiring a cruise button press.",
"SLCAutoConfirm",
),
self._toggle_item(
"IQ SLC Fallback Set Speed",
"If no speed limit is available, use the current set speed as the controller target.",
"SLCFallbackSetSpeed",
),
self._toggle_item(
"IQ SLC Fallback Previous",
"If a new limit is denied, reuse the previous accepted speed limit when possible.",
"SLCFallbackPreviousSpeedLimit",
),
self._toggle_item(
"IQ SLC Fallback Experimental",
"When no speed limit is available, let IQ.Dynamic request experimental longitudinal behavior.",
"SLCFallbackExperimentalMode",
),
self._toggle_item(
"IQ SLC Online Filler",
"Use online sources (TomTom + Mapbox) to fill in missing speed limits when local map data is unavailable.",
"SLCOnlineFiller",
),
self._option_item(
"IQ Map Lookahead Higher",
"How far ahead IQ.Pilot should apply an upcoming higher map speed limit before the limit changes.",
"MapSpeedLookaheadHigher",
100,
1000,
step=50,
use_float_scaling=True,
label_callback=seconds_label,
),
self._option_item(
"IQ Map Lookahead Lower",
"How far ahead IQ.Pilot should apply an upcoming lower map speed limit before the limit changes.",
"MapSpeedLookaheadLower",
100,
1000,
step=50,
use_float_scaling=True,
label_callback=seconds_label,
),
IQLineSeparator(60),
IQListItem(title=lambda: tr("IQ.Dynamic"), description="", action_item=None, inline=True,
title_color=rl.Color(16, 185, 169, 255)),
IQLineSeparator(20),
self._toggle_item(
"IQ.Dynamic Curves",
"Allow IQ.Dynamic to enter blended control for curves and strong vision slowdown cues.",
"IQDynamicConditionalCurves",
),
self._toggle_item(
"IQ.Dynamic Slower Lead",
"Allow IQ.Dynamic to switch toward blended control when a slower lead vehicle is detected.",
"IQDynamicConditionalSlowerLead",
),
self._toggle_item(
"IQ.Dynamic Stopped Lead",
"Allow IQ.Dynamic to react more aggressively when a lead vehicle is nearly stopped.",
"IQDynamicConditionalStoppedLead",
),
self._toggle_item(
"IQ.Dynamic Model Stops",
"Allow IQ.Dynamic to switch toward blended control for stop-sign and stop-light style vision stops.",
"IQDynamicConditionalModelStops",
),
self._toggle_item(
"IQ.Dynamic SLC Fallback",
"Allow IQ.Dynamic to request blended control when Speed Limit Controller has no usable target.",
"IQDynamicConditionalSLCFallback",
),
self._option_item(
"IQ.Dynamic Low Speed",
"Below this speed, IQ.Dynamic prefers blended control when no lead is present.",
"IQDynamicConditionalSpeed",
500,
3500,
step=50,
use_float_scaling=True,
label_callback=speed_label,
),
self._option_item(
"IQ.Dynamic Lead Speed",
"Below this speed, IQ.Dynamic prefers blended control even with a tracked lead.",
"IQDynamicConditionalLeadSpeed",
500,
4000,
step=50,
use_float_scaling=True,
label_callback=speed_label,
),
self._option_item(
"IQ.Dynamic Model Stop Time",
"Sets the vision stop prediction time horizon used by IQ.Dynamic and IQ Force Stops. Shorter values react later. Longer values react earlier.",
"IQDynamicModelStopTime",
100,
600,
step=25,
use_float_scaling=True,
label_callback=seconds_label,
),
self._toggle_item(
"IQ Force Stops",
"Bring the car to a complete stop for stop signs and stop lights, and feather the brake in the final meters of "
"every stop so it settles gently instead of rocking on its suspension. Yields full braking authority when a lead "
"is close. Override with the accelerator.",
"IQForceStops",
),
self._option_item(
"IQ Custom Stop Distance",
"Nudge how far back IQ.Pilot stops behind a stopped lead vehicle or a model-held stop (red light). Positive "
"stops further back, negative settles in closer. Works whether IQ Force Stops is on or off.",
"IQCustomStopDistance",
-2,
2,
step=1,
label_callback=distance_label,
),
]
return items
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()
class IQDeveloperLayout(DeveloperLayout):
def __init__(self):
super().__init__()
self.error_log_path = os.path.join(Paths.crash_log_root(), "error.log")
self._is_release_branch: bool = self._is_release or ui_state.params.get_bool("IsReleaseIqBranch")
self._is_development_branch: bool = ui_state.params.get_bool("IsTestedBranch") or ui_state.params.get_bool("IsDevelopmentBranch")
self._initialize_items()
for item in self.items:
self._scroller.add_widget(item)
def _initialize_items(self):
self.error_log_btn = button_item(lambda: tr("Crash Log"), lambda: tr("VIEW"),
lambda: tr("View the error log for IQ.Pilot crashes."), callback=self._on_error_log_clicked)
self.items: list = [self.error_log_btn]
def _on_delete_confirm(self, result):
if result == DialogResult.CONFIRM:
if os.path.exists(self.error_log_path):
os.remove(self.error_log_path)
def _on_error_log_closed(self, result, log_exists):
if result == DialogResult.CONFIRM and log_exists:
dialog2 = ConfirmDialog(tr("Delete this log?"), tr("Yes"), tr("No"), rich=False)
gui_app.set_modal_overlay(dialog2, callback=self._on_delete_confirm)
def _on_error_log_clicked(self):
text = ""
if os.path.exists(self.error_log_path):
text = f"{datetime.datetime.fromtimestamp(os.path.getmtime(self.error_log_path)).strftime('%d-%b-%Y %H:%M:%S').upper()}
"
try:
with open(self.error_log_path) as file:
text += file.read()
except Exception:
pass
dialog = NoticeModal(text=text, callback=lambda result: self._on_error_log_closed(result, os.path.exists(self.error_log_path)))
gui_app.set_modal_overlay(dialog)
def _update_state(self):
self.error_log_btn.set_visible(not self._is_release_branch)
UPDATES_DESCRIPTIONS = {
'disable_updates_offroad': tr_noop(
"Turns off over-the-air update checks entirely.
Reboot for this to take effect."
),
'disable_updates_onroad': tr_noop(
"Put the device in Always Offroad, or shut the car down, before touching these."
),
'install_mode_offroad': tr_noop(
"Pick whether a ready update just waits for your go-ahead, or downloads and installs on its own."
),
'install_mode_onroad': tr_noop(
"Put the device in Always Offroad, or shut the car down, before changing how updates install."
)
}
INSTALL_MODE_DOWNLOAD_ONLY = "download_only"
INSTALL_MODE_DOWNLOAD_AND_INSTALL = "download_and_install"
INSTALL_MODE_OPTIONS = [
INSTALL_MODE_DOWNLOAD_ONLY,
INSTALL_MODE_DOWNLOAD_AND_INSTALL,
]
INSTALL_MODE_LABELS = {
INSTALL_MODE_DOWNLOAD_ONLY: tr_noop("Predownload Only"),
INSTALL_MODE_DOWNLOAD_AND_INSTALL: tr_noop("Predownload + Preinstall"),
}
class IQSoftwareLayout(SoftwareLayout):
def __init__(self):
super().__init__()
self.disable_updates_toggle = toggle_item(
lambda: tr("Pause Updates"),
description="",
initial_state=ui_state.params.get_bool("DisableUpdates"),
callback=self._on_disable_updates_toggled,
)
install_mode = ui_state.params.get("UpdaterInstallMode") or INSTALL_MODE_DOWNLOAD_AND_INSTALL
if install_mode not in INSTALL_MODE_OPTIONS:
install_mode = INSTALL_MODE_DOWNLOAD_AND_INSTALL
self._install_mode_dialog = None
self.install_mode_btn = button_item(
lambda: tr("Update Install Mode"),
lambda: tr("CHANGE"),
description="",
callback=self._on_change_install_mode,
)
self.install_mode_btn.action_item.set_value(tr(INSTALL_MODE_LABELS[install_mode]))
try:
os_version = HARDWARE.get_os_version() or "unknown"
except Exception:
os_version = "unknown"
self.iqos_version_item = text_item(lambda: tr("IQ.OS Version"), os_version)
self._scroller.add_widget(self.iqos_version_item)
self._scroller.add_widget(self.disable_updates_toggle)
self._scroller.add_widget(self.install_mode_btn)
def _handle_reboot(self, result):
if result == DialogResult.CONFIRM:
ui_state.params.put_bool("DisableUpdates", self.disable_updates_toggle.action_item.get_state())
ui_state.params.put_bool("DoReboot", True)
else:
self.disable_updates_toggle.action_item.set_state(ui_state.params.get_bool("DisableUpdates"))
def _on_disable_updates_toggled(self, enabled):
dialog = ConfirmDialog(tr("This needs a reboot to apply. Restart now?"), tr("Reboot"))
gui_app.set_modal_overlay(dialog, callback=self._handle_reboot)
def _on_change_install_mode(self):
current_mode = ui_state.params.get("UpdaterInstallMode") or INSTALL_MODE_DOWNLOAD_AND_INSTALL
if current_mode not in INSTALL_MODE_OPTIONS:
current_mode = INSTALL_MODE_DOWNLOAD_AND_INSTALL
labels = [tr(INSTALL_MODE_LABELS[mode]) for mode in INSTALL_MODE_OPTIONS]
current_label = tr(INSTALL_MODE_LABELS[current_mode])
self._install_mode_dialog = MultiOptionDialog(tr("Update Install Mode"), labels, current_label)
def handle_selection(result):
if result == DialogResult.CONFIRM and self._install_mode_dialog is not None and self._install_mode_dialog.selection:
selected_label = self._install_mode_dialog.selection
for mode in INSTALL_MODE_OPTIONS:
if selected_label == tr(INSTALL_MODE_LABELS[mode]):
ui_state.params.put("UpdaterInstallMode", mode)
self.install_mode_btn.action_item.set_value(selected_label)
break
self._install_mode_dialog = None
gui_app.set_modal_overlay(self._install_mode_dialog, callback=handle_selection)
def _on_uninstall(self):
def handle_uninstall_confirmation(result):
if result == DialogResult.CONFIRM:
ui_state.params.put_bool("DoUninstall", True)
gui_app.request_close()
dialog = ConfirmDialog(tr("Are you sure you want to uninstall?"), tr("Uninstall"))
gui_app.set_modal_overlay(dialog, callback=handle_uninstall_confirmation)
def _on_select_branch(self):
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]
current_target = ui_state.params.get("UpdaterTargetBranch") or ""
top_level_branches = [current_git_branch, "release-mici", "release-tizi", "staging", "dev", "master"]
if HARDWARE.get_device_type() == "tici":
top_level_branches = ["release-new", "release-tici", "staging-tici"]
branches = [b for b in branches if b in ("release-new", "beta") or b.endswith("-tici")]
pinned_nodes = [PickerItem(b, {'display_name': b}) for b in top_level_branches if b in branches]
other_nodes = [PickerItem(b, {'display_name': b}) for b in sorted(branches)
if b not in top_level_branches and not b.endswith("-prebuilt")]
folders = [PickerGroup("", pinned_nodes + other_nodes)]
def _on_branch_selected(result):
if result == DialogResult.CONFIRM and self._branch_dialog is not None:
selection = self._branch_dialog.selection_ref
if 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
self._branch_dialog = PickerDialog(tr("Select a branch"), folders, current_target, "",
on_exit=_on_branch_selected)
gui_app.set_modal_overlay(self._branch_dialog, callback=_on_branch_selected)
def _update_state(self):
super()._update_state()
self.disable_updates_toggle.action_item.set_enabled(ui_state.is_offroad())
self.disable_updates_toggle.set_visible(True)
self.install_mode_btn.action_item.set_enabled(ui_state.is_offroad())
self.install_mode_btn.set_visible(True)
disable_updates_desc = tr(UPDATES_DESCRIPTIONS["disable_updates_offroad"] if ui_state.is_offroad() else UPDATES_DESCRIPTIONS["disable_updates_onroad"])
self.disable_updates_toggle.set_description(disable_updates_desc)
install_mode_desc = tr(UPDATES_DESCRIPTIONS["install_mode_offroad"] if ui_state.is_offroad() else UPDATES_DESCRIPTIONS["install_mode_onroad"])
self.install_mode_btn.set_description(install_mode_desc)
install_mode = ui_state.params.get("UpdaterInstallMode") or INSTALL_MODE_DOWNLOAD_AND_INSTALL
if install_mode not in INSTALL_MODE_OPTIONS:
install_mode = INSTALL_MODE_DOWNLOAD_AND_INSTALL
self.install_mode_btn.action_item.set_value(tr(INSTALL_MODE_LABELS[install_mode]))
waiting = getattr(self, "_waiting_for_updater", False)
self._download_btn.action_item.set_loading(waiting)
if waiting:
self._download_btn.action_item.set_enabled(False)
_OFFROAD_SHUTDOWN_MINUTES = (0, 5, 10, 15, 30, 60, 120, 180, 300, 600, 1440, 1800)
offroad_time_options = dict(enumerate(_OFFROAD_SHUTDOWN_MINUTES))
FORCE_ONROAD_DURATION_SEC = 10 * 60
FORCE_ONROAD_PARAM = "ForceOnroadUntil"
class IQDeviceLayout(DeviceLayout):
MENU_ROOT = 0
MENU_SYSTEM = 1
MENU_MAINTENANCE = 2
def __init__(self):
DeviceLayout.__init__(self)
self._scroller._line_separator = None
self._submenu = self.MENU_ROOT
def _initialize_items(self):
DeviceLayout._initialize_items(self)
self._always_offroad_btn = self._left_button(lambda: tr("Keep Device Offroad"), self._handle_always_offroad)
self._force_onroad_btn = self._left_button(lambda: tr("Force On-Road (10 min)"), self._handle_force_onroad)
self._max_time_offroad = option_item(
title=lambda: tr("Offroad Shutdown Timer"),
description=lambda: tr("Powers the device down once it has sat this long with the engine off.\n(30h by default)"),
param="MaxTimeOffroad",
min_value=0,
max_value=11,
value_change_step=1,
on_value_changed=None,
enabled=True,
icon="",
value_map=offroad_time_options,
label_width=360,
use_float_scaling=False,
inline=True,
label_callback=self._update_max_time_offroad_label
)
self._device_wake_mode = multiple_button_item(
title=lambda: tr("Boot State"),
description=self.wake_mode_description,
param="DeviceBootMode",
buttons=[lambda: tr("Default"), lambda: tr("Offroad")],
button_width=364,
callback=None,
inline=True,
)
self._change_language_btn = button_item(lambda: tr("Change Language"), lambda: tr("CHANGE"), callback=self._show_language_dialog)
self._driver_camera_btn = button_item(lambda: tr("Driver Camera Check"), lambda: tr("PREVIEW"), callback=self._show_driver_camera)
self._reg_and_training = self._left_button(lambda: tr("Regulatory"), self._on_regulatory)
self._onroad_uploads_and_reset_settings = dual_button_item(
left_text=lambda: tr("Upload While Driving"),
left_callback=lambda: ui_state.params.put_bool("OnroadUploads", not ui_state.params.get_bool("OnroadUploads")),
right_text=lambda: tr("Restore Defaults"),
right_callback=self._reset_settings
)
self._power_buttons = dual_button_item(
left_text=lambda: tr("Reboot"),
right_text=lambda: tr("Power Off"),
left_callback=self._reboot_prompt,
right_callback=self._power_off_prompt
)
self._submenu_system_btn = self._section_button(lambda: tr("System"), "icons/iq/sec_system.png", self.MENU_SYSTEM)
self._submenu_maintenance_btn = self._section_button(lambda: tr("Maintenance"), "icons/iq/sec_maintenance.png", self.MENU_MAINTENANCE)
self._submenu_back_btn = self._left_button(lambda: tr("Back"), self._go_back)
self._submenu_top_separator = LineSeparator(height=10)
self._submenu_gap = Spacer(10)
self._submenu_bottom_separator = LineSeparator(height=10)
self._system_sep_a = LineSeparator()
self._system_sep_b = LineSeparator()
self._system_sep_c = LineSeparator()
self._system_sep_d = LineSeparator()
items = [
text_item(lambda: tr("Dongle ID"), self._params.get("DongleId") or (lambda: tr("N/A"))),
LineSeparator(),
text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))),
self._system_sep_a,
self._reset_calib_btn,
self._system_sep_b,
self._change_language_btn,
self._system_sep_c,
self._device_wake_mode,
self._system_sep_d,
self._max_time_offroad,
self._submenu_top_separator,
self._submenu_system_btn,
self._submenu_maintenance_btn,
self._submenu_back_btn,
self._driver_camera_btn,
self._reg_and_training,
self._onroad_uploads_and_reset_settings,
self._submenu_gap,
self._submenu_bottom_separator,
self._power_buttons,
]
return items
def _left_button(self, text, callback):
item = dual_button_item(left_text=text, left_callback=callback, right_text="", right_callback=None)
item.action_item.right_button.set_visible(False)
return item
def _section_button(self, text, icon, submenu):
item = self._left_button(text, lambda: self._set_submenu(submenu))
item.action_item.left_button = NavSectionButton(text, icon, lambda: self._set_submenu(submenu))
return item
def _set_submenu(self, submenu: int):
self._submenu = submenu
self._scroller.scroll_panel.set_offset(0)
def _go_back(self):
self._set_submenu(self.MENU_ROOT)
def _offroad_transition(self):
self._power_buttons.action_item.right_button.set_visible(ui_state.is_offroad())
@staticmethod
def wake_mode_description() -> str:
def_str = tr("Default: comes up ready to drive, engagement available straight away.")
offrd_str = tr("Offroad: lands in Always Offroad every time it boots or wakes.")
header = tr("Sets which state the device settles into after a boot or a wake from sleep.")
return f"{header}\n\n{def_str}\n{offrd_str}"
@staticmethod
def _reset_settings():
def _do_reset(result: int):
if result == DialogResult.CONFIRM:
for _key in ui_state.params.all_keys():
ui_state.params.remove(_key)
HARDWARE.reboot()
def _second_confirm(result: int):
if result == DialogResult.CONFIRM:
gui_app.set_modal_overlay(ConfirmDialog(
text=tr("There's no undo once this runs — last chance to back out."),
confirm_text=tr("Confirm")
), callback=_do_reset)
gui_app.set_modal_overlay(ConfirmDialog(
text=tr("Are you sure you want to reset all IQ.Pilot settings to default? Once the settings are reset, there is no going back."),
confirm_text=tr("Reset")
), callback=_second_confirm)
@staticmethod
def _handle_always_offroad():
if ui_state.engaged:
gui_app.set_modal_overlay(alert_dialog(tr("Disengage before forcing offroad")))
return
_offroad_mode_state = ui_state.params.get_bool("IQAlwaysOffroad")
_offroad_mode_str = tr("Leave Always Offroad mode now?") if _offroad_mode_state else \
tr("Switch the device into Always Offroad mode?")
def _set_always_offroad(result: int):
if result == DialogResult.CONFIRM and not ui_state.engaged:
if _offroad_mode_state:
ui_state.params.put(FORCE_ONROAD_PARAM, 0)
ui_state.params.put_bool("IQAlwaysOffroad", not _offroad_mode_state)
gui_app.set_modal_overlay(ConfirmDialog(_offroad_mode_str, tr("Confirm")), callback=lambda result: _set_always_offroad(result))
@staticmethod
def _handle_force_onroad():
if ui_state.engaged:
gui_app.set_modal_overlay(alert_dialog(tr("Disengage before forcing on-road")))
return
if not ui_state.is_offroad():
gui_app.set_modal_overlay(alert_dialog(tr("Force On-Road can only be started while offroad")))
return
now = int(time.time())
force_onroad_until = ui_state.params.get(FORCE_ONROAD_PARAM, return_default=True)
is_active = force_onroad_until > now
if is_active:
prompt = tr("Force On-Road is already active. Reset the timer to 10 minutes?")
else:
prompt = tr("Force IQ.Pilot on-road for 10 minutes? Always Offroad will be enabled automatically and it will return to offroad after the timer.")
def _set_force_onroad(result: int):
if result == DialogResult.CONFIRM and not ui_state.engaged:
ui_state.params.put_bool("IQAlwaysOffroad", True)
ui_state.params.put(FORCE_ONROAD_PARAM, int(time.time()) + FORCE_ONROAD_DURATION_SEC)
gui_app.set_modal_overlay(ConfirmDialog(prompt, tr("Confirm")), callback=_set_force_onroad)
@staticmethod
def _update_max_time_offroad_label(value: int) -> str:
label = tr("Never Sleep") if value == 0 else f"{value}" + tr("m") if value < 60 else f"{value // 60}" + tr("h")
label += tr(" (Default)") if value == 1800 else ""
return label
def _update_state(self):
super()._update_state()
always_offroad = ui_state.params.get_bool("IQAlwaysOffroad")
now = int(time.time())
force_onroad_until = ui_state.params.get(FORCE_ONROAD_PARAM, return_default=True)
force_onroad_active = force_onroad_until > now
offroad_mode_btn_text = tr("Exit Offroad Mode") if always_offroad else tr("Keep Device Offroad")
offroad_mode_btn_style = ButtonStyle.PRIMARY if always_offroad else ButtonStyle.DANGER
self._always_offroad_btn.action_item.left_button.set_text(offroad_mode_btn_text)
self._always_offroad_btn.action_item.left_button.set_button_style(offroad_mode_btn_style)
if self._scroller._items.__contains__(self._always_offroad_btn):
self._scroller._items.remove(self._always_offroad_btn)
if ui_state.is_offroad() and not always_offroad:
self._scroller._items.insert(len(self._scroller._items) - 1, self._always_offroad_btn)
else:
self._scroller._items.insert(0, self._always_offroad_btn)
if self._scroller._items.__contains__(self._force_onroad_btn):
self._scroller._items.remove(self._force_onroad_btn)
self._scroller._items.insert(self._scroller._items.index(self._always_offroad_btn) + 1, self._force_onroad_btn)
if force_onroad_active:
remaining = force_onroad_until - now
minutes = remaining // 60
seconds = remaining % 60
force_onroad_text = f"{tr('Forced On-Road Active')} ({minutes}:{seconds:02d})"
force_onroad_style = ButtonStyle.PRIMARY
else:
force_onroad_text = tr("Force On-Road (10 min)")
force_onroad_style = ButtonStyle.NORMAL
self._force_onroad_btn.action_item.left_button.set_text(force_onroad_text)
self._force_onroad_btn.action_item.left_button.set_button_style(force_onroad_style)
self._force_onroad_btn.action_item.left_button.set_enabled(ui_state.is_offroad())
self._onroad_uploads_and_reset_settings.action_item.left_button.set_button_style(
ButtonStyle.PRIMARY if ui_state.params.get_bool("OnroadUploads") else ButtonStyle.NORMAL
)
self._driver_camera_btn.set_enabled(ui_state.is_offroad())
self._reg_and_training.action_item.left_button.set_enabled(ui_state.is_offroad())
self._onroad_uploads_and_reset_settings.action_item.right_button.set_enabled(ui_state.is_offroad())
root_menu = self._submenu == self.MENU_ROOT
system_menu = self._submenu == self.MENU_SYSTEM
maintenance_menu = self._submenu == self.MENU_MAINTENANCE
self._submenu_system_btn.set_visible(root_menu)
self._submenu_maintenance_btn.set_visible(root_menu)
self._submenu_back_btn.set_visible(not root_menu)
self._always_offroad_btn.set_visible(system_menu)
self._force_onroad_btn.set_visible(system_menu)
self._reset_calib_btn.set_visible(system_menu)
self._change_language_btn.set_visible(system_menu)
self._system_sep_a.set_visible(system_menu)
self._system_sep_b.set_visible(system_menu)
self._system_sep_c.set_visible(system_menu)
self._system_sep_d.set_visible(system_menu)
self._device_wake_mode.set_visible(system_menu)
self._max_time_offroad.set_visible(system_menu)
self._driver_camera_btn.set_visible(system_menu)
self._reg_and_training.set_visible(system_menu)
self._onroad_uploads_and_reset_settings.set_visible(maintenance_menu)
self._submenu_gap.set_visible(maintenance_menu)
self._submenu_bottom_separator.set_visible(maintenance_menu)
self._power_buttons.set_visible(maintenance_menu)
self._submenu_top_separator.set_visible(root_menu or system_menu or maintenance_menu)
if gui_app.iqpilot_ui():
from iqpilot.system.ui.iqwidgets.widgets.list_view import button_item as button_item
_ACTIVE_BUNDLE_KEY = "ModelManager_ActiveBundle"
_DOWNLOAD_INDEX_KEY = "ModelManager_DownloadIndex"
_RUNNER_CACHE_KEY = "ModelRunnerTypeCache"
def _big_model_options() -> list[tuple[str, str]]:
try:
from iqpilot.selfdrive.iqmodeld.emac_model_meta import big_models
return big_models(ui_state.params)
except Exception:
return []
def _big_model_label(key: str) -> str:
for name, display in _big_model_options():
if name == key:
return display
return key
def _refresh_big_catalog() -> None:
def worker():
try:
from iqpilot.selfdrive.iqmodeld.emac_model_meta import refresh_catalog
refresh_catalog(ui_state.params)
except Exception:
pass
threading.Thread(target=worker, daemon=True).start()
class ModelsLayout(Widget):
def __init__(self):
super().__init__()
self.model_manager = None
self._refreshing = False
self._refresh_start = 0.0
self.download_status = None
self.prev_download_status = None
self.model_dialog = None
self._big_model_dialog = None
self.last_cache_calc_time = 0
self._initialize_items()
self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB")
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
def _initialize_items(self):
self.current_model_item = IQListItem(
title=lambda: tr("Active Model"),
description="",
action_item=WideButtonAction(lambda: tr("SELECT")),
callback=self._handle_current_model_clicked
)
self.big_model_item = button_item(
lambda: tr("Big Model"),
lambda: tr("CHANGE"),
tr("Only works with external compute connected over USB."),
self._handle_big_model_clicked,
)
self.big_model_item.action_item.set_value(self._big_model_value())
self.supercombo_label = progress_item(tr("Combined Model"))
self.vision_label = progress_item(tr("Vision Weights"))
self.policy_label = progress_item(tr("Policy Weights"))
self.refresh_item = button_item(lambda: tr("Reload Model List"), lambda: tr("REFRESH"), "", self._on_refresh_models)
self.clear_cache_item = IQListItem(
title=lambda: tr("Purge Model Cache"),
description="",
action_item=WideButtonAction(lambda: tr("CLEAR")),
callback=self._clear_cache
)
self.redownload_item = button_item(lambda: tr("Redownload Current Model"), lambda: tr("REDOWNLOAD"), "", self._redownload_model)
self.cancel_download_item = button_item(tr("Stop Download"), tr("Cancel"), "", self._cancel_model_request)
self.items = [self.current_model_item, self.big_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label,
self.policy_label, self.redownload_item, self.refresh_item, self.clear_cache_item]
def _is_downloading(self):
return (self.model_manager and self.model_manager.selectedBundle and
self.model_manager.selectedBundle.status == custom.IQModelManager.DownloadStatus.downloading)
@staticmethod
def _has_download_request() -> bool:
try:
return int(ui_state.params.get(_DOWNLOAD_INDEX_KEY)) >= 0
except (TypeError, ValueError):
return False
@staticmethod
def _has_active_bundle_param() -> bool:
return bool(ui_state.params.get(_ACTIVE_BUNDLE_KEY))
def _has_model_request(self) -> bool:
return self._has_download_request()
@staticmethod
def _calculate_cache_size():
cache_size = 0.0
if os.path.exists(CUSTOM_MODEL_PATH):
cache_size = sum(os.path.getsize(os.path.join(CUSTOM_MODEL_PATH, file)) for file in os.listdir(CUSTOM_MODEL_PATH)) / (1024**2)
return cache_size
@staticmethod
def _bundle_index(bundle) -> int | None:
try:
return int(getattr(bundle, "index", -1))
except (TypeError, ValueError):
return None
@classmethod
def _bundle_matches(cls, left, right) -> bool:
if left is None or right is None:
return False
left_index = cls._bundle_index(left)
right_index = cls._bundle_index(right)
if left_index is not None and right_index is not None and left_index == right_index:
return True
for attr in ("ref", "internalName", "displayName"):
left_value = getattr(left, attr, None)
if left_value and left_value == getattr(right, attr, None):
return True
return False
@staticmethod
def _safe_model_path(filename: str) -> str | None:
if not filename or os.path.basename(filename) != filename:
return None
root = os.path.realpath(CUSTOM_MODEL_PATH)
path = os.path.realpath(os.path.join(root, filename))
try:
if os.path.commonpath([root, path]) != root:
return None
except ValueError:
return None
return path
def _remove_bundle_files(self, bundle) -> None:
for model in getattr(bundle, "models", []) or []:
for artifact in (getattr(model, "metadata", None), getattr(model, "artifact", None)):
filename = getattr(artifact, "fileName", "") if artifact is not None else ""
path = self._safe_model_path(filename)
if path is None:
continue
for candidate in (path, f"{path}.download"):
try:
if os.path.isfile(candidate):
os.remove(candidate)
except OSError:
pass
def _clear_cache(self):
def _callback(response):
if response == DialogResult.CONFIRM:
ui_state.params.put_bool("ModelManager_ClearCache", True)
self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB")
gui_app.set_modal_overlay(ConfirmDialog(tr("This will delete ALL downloaded models from the cache except the currently active model. Are you sure?"),
tr("Clear Cache")), callback=_callback)
def _redownload_target_bundle(self):
if not self.model_manager:
return None
selected = self.model_manager.selectedBundle
if selected and selected.status == custom.IQModelManager.DownloadStatus.failed:
return selected
active = self.model_manager.activeBundle
if self._has_active_bundle_param() and active and active.ref:
return active
return None
def _redownload_target_index(self) -> int | None:
target = self._redownload_target_bundle()
if not target:
return None
try:
return int(target.index)
except (TypeError, ValueError):
pass
for bundle in self.model_manager.availableBundles:
if bundle.ref and bundle.ref == target.ref:
return int(bundle.index)
if bundle.internalName and bundle.internalName == target.internalName:
return int(bundle.index)
return None
def _can_redownload(self) -> bool:
return bool(ui_state.is_offroad() and not self._is_downloading() and not self._has_model_request() and self._redownload_target_index() is not None)
def _cancel_model_request(self):
ui_state.params.remove(_DOWNLOAD_INDEX_KEY)
def _redownload_model(self):
index = self._redownload_target_index()
if index is None:
return
def _callback(response):
if response == DialogResult.CONFIRM:
target = self._redownload_target_bundle()
if target is not None:
self._remove_bundle_files(target)
if self._bundle_matches(getattr(self.model_manager, "activeBundle", None), target):
ui_state.params.remove(_ACTIVE_BUNDLE_KEY)
ui_state.params.remove(_RUNNER_CACHE_KEY)
ui_state.params.put(_DOWNLOAD_INDEX_KEY, index)
gui_app.set_modal_overlay(ConfirmDialog(tr("Clear the selected model cache and download it again?"),
tr("Redownload")), callback=_callback)
def _handle_bundle_download_progress(self):
labels = {custom.IQModelManager.Model.Type.supercombo: self.supercombo_label,
custom.IQModelManager.Model.Type.vision: self.vision_label,
custom.IQModelManager.Model.Type.policy: self.policy_label}
for label in labels.values():
label.set_visible(False)
self.cancel_download_item.set_visible(False)
if not self.model_manager or (not self.model_manager.selectedBundle and (not self._has_active_bundle_param() or not self.model_manager.activeBundle)):
return
bundle = self.model_manager.selectedBundle if self._is_downloading() or (
self.model_manager.selectedBundle and self.model_manager.selectedBundle.status == custom.IQModelManager.DownloadStatus.failed
) else (self.model_manager.activeBundle if self._has_active_bundle_param() else None)
if not bundle:
return
self.download_status = bundle.status
status_changed = self.prev_download_status != self.download_status
self.prev_download_status = self.download_status
self.cancel_download_item.set_visible(bool(self.model_manager.selectedBundle) and self._has_download_request())
if (current_time := time.monotonic()) - self.last_cache_calc_time > 0.5:
self.last_cache_calc_time = current_time
self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB")
if self.download_status == custom.IQModelManager.DownloadStatus.downloading:
device._reset_interactive_timeout()
DS = custom.IQModelManager.DownloadStatus
bundle_downloading = bundle.status == DS.downloading
for model in bundle.models:
label = labels.get(getattr(model.type, 'raw', model.type))
if label is None:
continue
label.set_visible(True)
p = model.artifact.downloadProgress
text, show, color, indeterminate = self._model_label_state(p, bundle, bundle_downloading, status_changed)
label.action_item.update(p.progress, text, show, color, indeterminate=indeterminate)
def _model_label_state(self, p, bundle, bundle_downloading, status_changed):
DS = custom.IQModelManager.DownloadStatus
name = bundle.displayName
live = p.status == DS.downloading or (bundle_downloading and p.status not in (DS.downloaded, DS.cached, DS.failed))
if live:
if p.progress > 0:
return f"{int(p.progress)}% - {name}", True, rl.GRAY, False
return f"{tr('downloading')} - {name}", True, rl.GRAY, True
if p.status in (DS.downloaded, DS.cached):
status_text = tr("from cache" if p.status == DS.cached else "downloaded")
return f"{name} - {status_text if status_changed else tr('ready')}", False, ON_COLOR, False
if p.status == DS.failed:
return f"download failed - {name}", False, rl.RED, False
return f"pending - {name}", False, rl.GRAY, False
@staticmethod
def _show_reset_params_dialog():
def _callback(response):
if response == DialogResult.CONFIRM:
ui_state.params.remove("CalibrationParams")
ui_state.params.remove("LiveTorqueParameters")
msg = tr("The selected model changed. We suggest resetting calibration. Would you like to do that now?")
gui_app.set_modal_overlay(ConfirmDialog(msg, tr("Reset Calibration")), callback=_callback)
def _on_model_selected(self, result):
if result != DialogResult.CONFIRM:
return
selected_ref = self.model_dialog.selection_ref
if selected_ref == "Default":
active = self.model_manager.activeBundle if self.model_manager else None
had_custom_model = bool(active and active.ref and not is_default_bundle(active))
select_default_model(ui_state.params)
if had_custom_model:
self._show_reset_params_dialog()
elif selected_bundle := next((bundle for bundle in self.model_manager.availableBundles if bundle.ref == selected_ref), None):
ui_state.params.put(_DOWNLOAD_INDEX_KEY, selected_bundle.index)
if self.model_manager.activeBundle and selected_bundle.generation != self.model_manager.activeBundle.generation:
self._show_reset_params_dialog()
self.model_dialog = None
@staticmethod
def _bundle_to_node(bundle):
return PickerItem(bundle.ref, {'display_name': bundle.displayName, 'short_name': bundle.internalName})
def _get_folders(self, favorites):
bundles = self.model_manager.availableBundles
folders = {}
for bundle in bundles:
folders.setdefault(next((ov_ride.value for ov_ride in bundle.overrides if ov_ride.key == "folder"), ""), []).append(bundle)
folders_list = [PickerGroup("", [PickerItem("Default", {'display_name': tr("Default (CD210)"), 'short_name': "Default"})])]
for folder, folder_bundles in sorted(folders.items(), key=lambda x: max((bundle.index for bundle in x[1]), default=-1), reverse=True):
folder_bundles.sort(key=lambda bundle: bundle.index, reverse=True)
name = folder + (f" - (Updated: {m.group(1)})" if folder_bundles and (m := re.search(r'\(([^)]*)\)[^(]*$', folder_bundles[0].displayName)) else "")
folders_list.append(PickerGroup(name, [self._bundle_to_node(bundle) for bundle in folder_bundles]))
if favorites and (fav_bundles := [bundle for bundle in bundles if bundle.ref in favorites]):
folders_list.insert(1, PickerGroup("Favorites", [self._bundle_to_node(bundle) for bundle in fav_bundles]))
return folders_list
def _handle_current_model_clicked(self):
favs = ui_state.params.get("IQModelFavorites")
favorites = set(favs.split(';')) if favs else set()
folders_list = self._get_folders(favorites)
active_ref = self.model_manager.activeBundle.ref if self._has_active_bundle_param() and self.model_manager.activeBundle else "Default"
self.model_dialog = PickerDialog(tr("Choose a Model"), folders_list, active_ref, "IQModelFavorites",
get_folders_fn=self._get_folders, on_exit=self._on_model_selected)
gui_app.set_modal_overlay(self.model_dialog, callback=self._on_model_selected)
@staticmethod
def _big_model_value() -> str:
if not ui_state.params.get_bool("IQEmacEnabled"):
return tr("Off")
key = ui_state.params.get("IQEmacModel")
key = key.decode() if isinstance(key, bytes) else (key or "")
return _big_model_label(key) if key in [n for n, _ in _big_model_options()] else tr("Off")
def _handle_big_model_clicked(self):
options = _big_model_options()
if len(options) <= 1:
_refresh_big_catalog()
keys = [n for n, _ in options]
labels = [tr("Off")] + [d for _, d in options]
self._big_model_dialog = MultiOptionDialog(tr("Big Model"), labels, self._big_model_value())
def handle_selection(result):
if result == DialogResult.CONFIRM and self._big_model_dialog is not None and self._big_model_dialog.selection:
selected = self._big_model_dialog.selection
if selected == tr("Off"):
ui_state.params.put_bool("IQEmacEnabled", False)
else:
for key in keys:
if _big_model_label(key) == selected:
ui_state.params.put("IQEmacModel", key)
ui_state.params.put_bool("IQEmacEnabled", True)
break
self.big_model_item.action_item.set_value(self._big_model_value())
self._big_model_dialog = None
gui_app.set_modal_overlay(self._big_model_dialog, callback=handle_selection)
def _on_refresh_models(self):
_refresh_big_catalog()
ui_state.params.put("ModelManager_LastSyncTime", 0)
self._refreshing = True
self._refresh_start = time.monotonic()
self.refresh_item.action_item.set_enabled(False)
def _update_refresh_state(self):
if not self._refreshing:
return
try:
last_sync = int(ui_state.params.get("ModelManager_LastSyncTime", return_default=True) or 0)
except (TypeError, ValueError):
last_sync = 0
elapsed = time.monotonic() - self._refresh_start
if (last_sync > 0 and elapsed > 1.0) or elapsed > 20.0:
self._refreshing = False
self.refresh_item.action_item.set_loading(False)
self.refresh_item.action_item.set_enabled(True)
else:
self.refresh_item.action_item.set_loading(True)
self.refresh_item.action_item.set_enabled(False)
def _update_state(self):
self._update_refresh_state()
self.model_manager = ui_state.sm["iqModelManager"]
self._handle_bundle_download_progress()
active = self.model_manager.activeBundle if self.model_manager else None
if is_default_bundle(active):
active_name = active.displayName or tr("Default (CD210)")
elif self._has_active_bundle_param() and active and active.ref:
active_name = active.internalName
else:
active_name = tr("Default (CD210)")
self.current_model_item.action_item.set_value(active_name)
self.redownload_item.action_item.set_enabled(self._can_redownload())
target = self._redownload_target_bundle()
self.redownload_item.action_item.set_value(target.internalName if target else "")
if not ui_state.is_offroad():
self.current_model_item.action_item.set_enabled(False)
self.current_model_item.set_description(tr("Reachable only with the car switched off or Always Offroad turned on."))
else:
self.current_model_item.action_item.set_enabled(True)
self.current_model_item.set_description("")
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()
OP.PANEL_COLOR = rl.Color(10, 10, 10, 255)
ICON_SIZE = 70
OP.PanelType = IntEnum(
"PanelType",
[es.name for es in OP.PanelType] + [
"MODELS",
"CRUISE",
"STEERING",
"VISUALS",
"NAVIGATION",
"VEHICLE",
],
start=0,
)
@dataclass
class PanelInfo(OP.PanelInfo):
icon: str = ""
class SidebarEntry(Widget):
def __init__(self, hub, panel_type, panel_info):
super().__init__()
self._hub = hub
self.panel_type = panel_type
self.panel_info = panel_info
@staticmethod
def _draw_active_pill(rect: rl.Rectangle, left_x: float):
pill = rl.Rectangle(left_x - 50, rect.y, OP.SIDEBAR_WIDTH - 50, OP.NAV_BTN_HEIGHT)
rl.draw_rectangle_rounded(pill, 0.2, 5, OP.CLOSE_BTN_COLOR)
def _render(self, rect):
active = self.panel_type == self._hub._current_panel
x = rect.x + 90
if active:
self._draw_active_pill(rect, x)
if self.panel_info.icon:
icon = gui_app.texture(self.panel_info.icon, ICON_SIZE, ICON_SIZE, keep_aspect_ratio=True)
rl.draw_texture(icon, int(x), int(rect.y + (OP.NAV_BTN_HEIGHT - icon.height) / 2), rl.WHITE)
x += ICON_SIZE + 20
label_h = measure_text_cached(self._hub._font_medium, self.panel_info.name, 65).y
rl.draw_text_ex(self._hub._font_medium, self.panel_info.name,
rl.Vector2(x, rect.y + (OP.NAV_BTN_HEIGHT - label_h) / 2), 55, 0,
OP.TEXT_SELECTED if active else OP.TEXT_NORMAL)
self.panel_info.button_rect = rect
class IQSettingsLayout(OP.SettingsLayout):
def __init__(self):
OP.SettingsLayout.__init__(self)
self._nav_items: list[Widget] = []
self._sidebar_scroller = Scroller([], spacing=0, line_separator=False, pad_end=False)
wifi_manager = WifiManager()
wifi_manager.set_active(False)
iq_asset = "offroad"
self._panels = {
OP.PanelType.DEVICE: PanelInfo(tr_noop("Device"), IQDeviceLayout(), icon=f"{iq_asset}/icon_home.png"),
OP.PanelType.NETWORK: PanelInfo(tr_noop("Network"), IQNetworkUI(wifi_manager), icon="icons/network.png"),
OP.PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout(), icon=f"{iq_asset}/icon_toggle.png"),
OP.PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), IQSoftwareLayout(), icon=f"{iq_asset}/icon_software.png"),
OP.PanelType.MODELS: PanelInfo(tr_noop("Models"), ModelsLayout(), icon=f"{iq_asset}/icon_models.png"),
OP.PanelType.CRUISE: PanelInfo(tr_noop("Cruise"), CruiseLayout(), icon=f"{iq_asset}/icon_longitudinal.png"),
OP.PanelType.STEERING: PanelInfo(tr_noop("Steering"), SteeringLayout(), icon="icons_mici/wheel.png"),
OP.PanelType.VISUALS: PanelInfo(tr_noop("Visuals"), VisualsLayout(), icon=f"{iq_asset}/icon_visuals.png"),
OP.PanelType.VEHICLE: PanelInfo(tr_noop("Vehicle"), VehicleLayout(), icon=f"{iq_asset}/icon_vehicle.png"),
OP.PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), IQDeveloperLayout(), icon="icons/shell.png"),
}
self._hidden_from_sidebar = {OP.PanelType.CRUISE}
self._panels[OP.PanelType.TOGGLES].instance.set_cruise_panel_callback(
lambda: self.set_current_panel(OP.PanelType.CRUISE)
)
def _sidebar_entries(self):
for panel_type, panel_info in self._panels.items():
if panel_type not in self._hidden_from_sidebar:
yield panel_type, panel_info
def _populate_sidebar(self, rect: rl.Rectangle):
for panel_type, panel_info in self._sidebar_entries():
entry = SidebarEntry(self, panel_type, panel_info)
entry.rect.width = rect.width - 100
entry.rect.height = OP.NAV_BTN_HEIGHT
self._nav_items.append(entry)
self._sidebar_scroller.add_widget(entry)
def _draw_close_button(self, rect: rl.Rectangle) -> rl.Rectangle:
btn = rl.Rectangle(rect.x + metrics.GUTTER * 3, rect.y + metrics.GUTTER * 2,
metrics.CLOSE_BTN, metrics.CLOSE_BTN)
pressed = (rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and
rl.check_collision_point_rec(rl.get_mouse_position(), btn))
rl.draw_rectangle_rounded(btn, 1.0, 20, OP.CLOSE_BTN_PRESSED if pressed else OP.CLOSE_BTN_COLOR)
icon = self._close_icon
dest = rl.Rectangle(btn.x + (btn.width - icon.width) / 2, btn.y + (btn.height - icon.height) / 2,
icon.width, icon.height)
tint = rl.Color(220, 220, 220, 255) if pressed else rl.WHITE
rl.draw_texture_pro(icon, rl.Rectangle(0, 0, icon.width, icon.height), dest, rl.Vector2(0, 0), 0, tint)
return btn
def _draw_sidebar(self, rect: rl.Rectangle):
rl.draw_rectangle_rec(rect, OP.SIDEBAR_COLOR)
self._close_btn_rect = self._draw_close_button(rect)
if not self._nav_items:
self._populate_sidebar(rect)
nav_rect = rl.Rectangle(rect.x, self._close_btn_rect.height + metrics.GUTTER * 4,
rect.width, rect.height - 300)
self._sidebar_scroller.render(nav_rect)
def _handle_mouse_release(self, mouse_pos: MousePos) -> bool:
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
if self._close_callback:
self._close_callback()
return True
if self._sidebar_scroller.scroll_panel.is_touch_valid():
for panel_type, panel_info in self._sidebar_entries():
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect):
self.set_current_panel(panel_type)
return True
return False
def show_event(self):
super().show_event()
self._panels[self._current_panel].instance.show_event()
self._sidebar_scroller.show_event()
class BrandPanel:
def __init__(self):
self.items: list = []
def update_settings(self) -> None:
pass
_TUNING_BLURBS = (
lambda: tr("Factory-default longitudinal tuning."),
lambda: tr("Dynamic tuning: livelier gas/brake response."),
lambda: tr("Predictive tuning: anticipates and smooths accel changes."),
)
class HyundaiSettings(BrandPanel):
def __init__(self):
super().__init__()
self.alpha_long_available = False
self.longitudinal_tuning_item = multiple_button_item(
tr("Longitudinal Tune Profile"), "",
[tr("Off"), tr("Dynamic"), tr("Predictive")],
button_width=300, param="IQHyundaiLongTune", inline=False,
callback=lambda index: ui_state.params.put("IQHyundaiLongTune", index))
self.items = [self.longitudinal_tuning_item]
def _alpha_long_supported(self) -> bool:
if bundle := ui_state.params.get("CarPlatformBundle"):
return CAR[bundle.get("platform")] not in (UNSUPPORTED_LONGITUDINAL_CAR | CANFD_UNSUPPORTED_LONGITUDINAL_CAR)
if ui_state.CP:
return ui_state.CP.alphaLongitudinalAvailable
return False
def update_settings(self):
self.alpha_long_available = self._alpha_long_supported()
selected = int(ui_state.params.get("IQHyundaiLongTune") or "0")
if not ui_state.is_offroad():
desc, usable = tr("Unavailable while the car is onroad."), False
elif not ui_state.has_longitudinal_control:
desc, usable = tr("Requires IQ.Pilot Longitudinal Control (Alpha) to be enabled."), False
else:
blurb = _TUNING_BLURBS[selected] if selected < len(_TUNING_BLURBS) else _TUNING_BLURBS[0]
desc, usable = blurb(), True
row = self.longitudinal_tuning_item
row.action_item.set_enabled(usable)
row.set_description(desc)
row.show_description(True)
row.action_item.set_selected_button(selected)
row.set_visible(self.alpha_long_available)
_UNSUPPORTED_FLAGS = SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID
class SubaruSettings(BrandPanel):
def __init__(self):
super().__init__()
self._supported = False
self.stop_and_go_toggle = toggle_item(tr("Creep from Standstill (Beta)"), "", param="IQSubaruCreepAssist",
callback=lambda _: self.update_settings())
self.stop_and_go_manual_parking_brake_toggle = toggle_item(
tr("Creep from Standstill — Manual Handbrake (Beta)"), "",
param="IQSubaruCreepAssistManualBrake", callback=lambda _: self.update_settings())
self.items = [self.stop_and_go_toggle, self.stop_and_go_manual_parking_brake_toggle]
def _platform_flags(self) -> int:
if bundle := ui_state.params.get("CarPlatformBundle"):
return CAR[bundle.get("platform")].config.flags
if ui_state.CP:
return ui_state.CP.flags
return 0
def _blocker_text(self) -> str:
if not self._supported:
return tr("Not available on this Subaru platform.")
if not ui_state.is_offroad():
return tr("Flip on Always Offroad from the Device panel, or power the car down, to change this.")
return ""
def update_settings(self):
self._supported = not (self._platform_flags() & _UNSUPPORTED_FLAGS)
blocker = self._blocker_text()
usable = self._supported and ui_state.is_offroad()
rows = (
(self.stop_and_go_toggle,
tr("Automatically resume from a stop while following traffic, on Subaru platforms where "
"the beta implementation applies.")),
(self.stop_and_go_manual_parking_brake_toggle,
tr("Stop-and-go variant for Subaru Global cars with a manual handbrake. Leave this off on "
"cars with an electric parking brake. Thanks to martinl for this implementation!")),
)
for row, body in rows:
row.action_item.set_enabled(usable)
row.set_description(f"{blocker}
{body}" if blocker else body)
COOP_STEERING_MIN_KMH = 23
OEM_STEERING_MIN_KMH = 48
KM_TO_MILE = 0.621371
def _speed_text(kmh: int) -> str:
if ui_state.is_metric:
return f"{kmh} km/h"
return f"{round(kmh * KM_TO_MILE)} mph"
class TeslaSettings(BrandPanel):
def __init__(self):
super().__init__()
self.torque_blend_toggle = toggle_item(tr("VTB (Virtual Torque Blending)"), "", param="IQTeslaTorqueBlend")
self.fsd_visualization_toggle = toggle_item(tr("FSD Visualization"), "", param="IQTeslaFsdVisualization")
self.items = [self.torque_blend_toggle, self.fsd_visualization_toggle]
def update_settings(self):
caution = tr("Warning: steering may oscillate in turns below {}; turn this off if you feel it.").format(
_speed_text(OEM_STEERING_MIN_KMH))
body = (f"{caution}
"
f"{tr('Lets you nudge the wheel while engaged without fully disengaging steering.')}
"
f"{tr('Active above {} only.').format(_speed_text(COOP_STEERING_MIN_KMH))}")
if not ui_state.is_offroad():
blocker = tr("Flip on Always Offroad from the Device panel, or power the car down, to change this.")
body = f"{blocker}
{body}"
self.torque_blend_toggle.set_description(body)
visualization_body = tr("Shows Tesla's FSD / Autosteer visualization while IQ.Pilot controls steering. " +
"Requires an offroad restart or reboot.")
if not ui_state.is_offroad():
visualization_body = f"{blocker}
{visualization_body}"
self.fsd_visualization_toggle.set_description(visualization_body)
for row in self.items:
row.action_item.set_enabled(ui_state.is_offroad())
class ToyotaSettings(BrandPanel):
def __init__(self):
super().__init__()
self.enforce_stock_longitudinal = toggle_item(
lambda: tr("Keep Factory Gas and Brake"),
description=lambda: tr("Keeps gas and brakes with the factory Toyota system; IQ.Pilot steers only."),
initial_state=ui_state.params.get_bool("IQToyotaFactoryLong"),
callback=self._on_toggled,
enabled=lambda: not ui_state.engaged,
)
self.items = [self.enforce_stock_longitudinal]
@staticmethod
def _apply(enabled: bool):
ui_state.params.put_bool("IQToyotaFactoryLong", enabled)
if enabled and ui_state.params.get_bool("AlphaLongitudinalEnabled"):
ui_state.params.put_bool("AlphaLongitudinalEnabled", False)
ui_state.params.put_bool("OnroadCycleRequested", True)
def _on_toggled(self, state: bool):
if not state:
self._apply(False)
return
def after_confirm(result: int):
if result == DialogResult.CONFIRM:
self._apply(True)
else:
self.enforce_stock_longitudinal.action_item.set_state(False)
row = self.enforce_stock_longitudinal
prompt = f"
{row.description}
" gui_app.set_modal_overlay(ConfirmDialog(prompt, tr("Enable"), rich=True), callback=after_confirm) DESCRIPTIONS = { 'pqhca5or7Toggle': tr_noop( 'Use HCA Status 7 instead of Status 5 for steering control on PQ platform vehicles. ' 'This may help with compatibility on some older Volkswagen models.' ), 'AllowLateralWhenLongUnavailable': tr_noop( 'Allow lateral control (steering) to remain active even when longitudinal control (gas/brake) ' 'is temporarily unavailable due to a cruise control fault.' ), 'iqMqbAccResume': tr_noop( 'Allow IQ.Pilot to use MQB ACC resume behavior on supported FtS with Extended Hold without Auto Resume Volkswagen MQB vehicles.' ), 'iqMqbSteeringLockout': tr_noop( 'Enable MQB steering lockout handling on Volkswagen MQB vehicles with low speed LKAS faults.' ), 'EnableCurvatureController': tr_noop( 'Close the steering loop on MEB and MQB Evo vehicles. IQ.Pilot corrects its steering command against ' 'the curvature the car is actually holding, instead of sending the planned curvature straight through.' ), 'EnableSmoothSteer': tr_noop( 'Low-pass the planned curvature before it reaches the rack on MEB and MQB Evo vehicles, which damps ' 'lateral wobble. On by default for these cars.' ), } class VolkswagenSettings(BrandPanel): def __init__(self): super().__init__() self.pq_hca_toggle = toggle_item( lambda: tr("PQ HCA Status 7 Mode"), description=lambda: tr(DESCRIPTIONS["pqhca5or7Toggle"]), initial_state=ui_state.params.get_bool("pqhca5or7Toggle"), callback=self._on_pq_hca_toggle, enabled=lambda: not ui_state.engaged, ) self.lateral_when_long_unavailable = toggle_item( lambda: tr("Lateral Control When Cruise Faulted"), description=lambda: tr(DESCRIPTIONS["AllowLateralWhenLongUnavailable"]), initial_state=ui_state.params.get_bool("AllowLateralWhenLongUnavailable"), callback=self._on_lateral_when_long_unavailable, enabled=lambda: not ui_state.engaged, ) self.mqb_acc_resume = toggle_item( lambda: tr("MQB ACC Resume"), description=lambda: tr(DESCRIPTIONS["iqMqbAccResume"]), initial_state=ui_state.params.get_bool("iqMqbAccResume"), callback=self._on_mqb_acc_resume, enabled=lambda: not ui_state.engaged, ) self.mqb_steering_lockout = toggle_item( lambda: tr("MQB Steering Lockout"), description=lambda: tr(DESCRIPTIONS["iqMqbSteeringLockout"]), initial_state=ui_state.params.get_bool("iqMqbSteeringLockout"), callback=self._on_mqb_steering_lockout, enabled=lambda: not ui_state.engaged, ) self.curvature_controller = toggle_item( lambda: tr("Curvature Controller"), description=lambda: tr(DESCRIPTIONS["EnableCurvatureController"]), initial_state=ui_state.params.get_bool("EnableCurvatureController"), callback=self._on_curvature_controller, enabled=lambda: not ui_state.engaged, ) self.smooth_steer = toggle_item( lambda: tr("Smooth Steering"), description=lambda: tr(DESCRIPTIONS["EnableSmoothSteer"]), initial_state=ui_state.params.get_bool("EnableSmoothSteer"), callback=self._on_smooth_steer, enabled=lambda: not ui_state.engaged, ) self.items = [self.pq_hca_toggle, self.lateral_when_long_unavailable, self.mqb_acc_resume, self.mqb_steering_lockout, self.curvature_controller, self.smooth_steer] def _flags(self) -> VolkswagenFlags: bundle = ui_state.params.get("CarPlatformBundle") if bundle: platform = bundle.get("platform") if platform: try: return CAR[platform].config.flags except (KeyError, AttributeError): return VolkswagenFlags(0) elif ui_state.CP: return ui_state.CP.flags return VolkswagenFlags(0) def _uses_hca_status_toggle(self) -> bool: return bool(self._flags() & (VolkswagenFlags.PQ | VolkswagenFlags.MLB)) def _is_mqb(self) -> bool: flags = self._flags() return not bool(flags & (VolkswagenFlags.PQ | VolkswagenFlags.MLB | VolkswagenFlags.MEB | VolkswagenFlags.MEB_GEN2 | VolkswagenFlags.MQB_EVO)) def _supports_lateral_when_faulted(self) -> bool: return not bool(self._flags() & VolkswagenFlags.MLB) def _is_curvature_car(self) -> bool: return bool(self._flags() & (VolkswagenFlags.MEB | VolkswagenFlags.MQB_EVO)) def _on_pq_hca_toggle(self, state: bool): ui_state.params.put_bool("pqhca5or7Toggle", state) def _on_lateral_when_long_unavailable(self, state: bool): ui_state.params.put_bool("AllowLateralWhenLongUnavailable", state) def _on_mqb_acc_resume(self, state: bool): ui_state.params.put_bool("iqMqbAccResume", state) def _on_mqb_steering_lockout(self, state: bool): ui_state.params.put_bool("iqMqbSteeringLockout", state) def _on_curvature_controller(self, state: bool): ui_state.params.put_bool("EnableCurvatureController", state) def _on_smooth_steer(self, state: bool): ui_state.params.put_bool("EnableSmoothSteer", state) def update_settings(self): self.pq_hca_toggle.set_visible(self._uses_hca_status_toggle()) self.lateral_when_long_unavailable.set_visible(self._supports_lateral_when_faulted()) is_mqb = self._is_mqb() self.mqb_acc_resume.set_visible(is_mqb) self.mqb_steering_lockout.set_visible(is_mqb) is_curvature_car = self._is_curvature_car() self.curvature_controller.set_visible(is_curvature_car) self.smooth_steer.set_visible(is_curvature_car) _REGISTRY: dict[str, type[BrandPanel]] = { "hyundai": HyundaiSettings, "subaru": SubaruSettings, "tesla": TeslaSettings, "toyota": ToyotaSettings, "volkswagen": VolkswagenSettings, } def brand_settings_for(brand: str) -> BrandPanel | None: cls = _REGISTRY.get(brand) return cls() if cls else None class BrandPanelFactory: create_brand_settings = staticmethod(brand_settings_for) class FingerprintStatus(IntEnum): NONE = 0 # nothing identified, nothing forced AUTO = 1 # car identified itself over CAN FORCED = 2 # user pinned a platform manually STATUS_COLORS = { FingerprintStatus.NONE: ink.STATUS_WARN, FingerprintStatus.AUTO: ink.STATUS_GOOD, FingerprintStatus.FORCED: ink.STATUS_INFO, } class VehicleSelection: def __init__(self): self.platforms: dict = load_catalog() @staticmethod def forced_bundle(): return ui_state.params.get("CarPlatformBundle") def status(self) -> FingerprintStatus: if self.forced_bundle(): return FingerprintStatus.FORCED if ui_state.CP and ui_state.CP.carFingerprint != "MOCK": return FingerprintStatus.AUTO return FingerprintStatus.NONE def display_name(self) -> str: if bundle := self.forced_bundle(): return bundle.get("name", "") if ui_state.CP and ui_state.CP.carFingerprint != "MOCK": return ui_state.CP.carFingerprint return tr("No vehicle set") def force(self, platform_name: str) -> bool: data = self.platforms.get(platform_name) if not data: return False ui_state.params.put("CarPlatformBundle", {**data, "name": platform_name}) return True @staticmethod def clear(): ui_state.params.remove("CarPlatformBundle") def picker_folders(self) -> list[PickerGroup]: def node_for(name: str) -> PickerItem: info = self.platforms[name] years = ' '.join(map(str, info.get('year', []))) return PickerItem(name, { 'display_name': name, 'search_tags': f"{name} {info.get('make')} {years} {info.get('model', name)}", }) names = sorted(self.platforms) makes = sorted({self.platforms[n].get('make') for n in names}) return [PickerGroup(make, [node_for(n) for n in names if self.platforms[n].get('make') == make]) for make in makes] class VehiclePicker(Button): def __init__(self, on_platform_change: Callable[[], None] | None = None): super().__init__(tr("Vehicle"), self._on_clicked, button_style=ButtonStyle.NORMAL) self.set_rect(rl.Rectangle(0, 0, 0, 120)) self.selection = VehicleSelection() self._on_platform_change = on_platform_change self.refresh() @property def text(self): return self._label._text @property def status(self) -> FingerprintStatus: return self.selection.status() @property def color(self) -> rl.Color: return STATUS_COLORS[self.status] def set_parent_rect(self, parent_rect): super().set_parent_rect(parent_rect) self._rect.width = parent_rect.width def refresh(self): self.set_text(self.selection.display_name()) self.set_enabled(True) def _notify(self): self.refresh() if self._on_platform_change: self._on_platform_change() def _on_clicked(self): if self.selection.forced_bundle(): self.selection.clear() self._notify() else: self._open_picker() def _open_picker(self): dialog = PickerDialog( tr("Pick your vehicle"), self.selection.picker_folders(), search_prompt=tr("Search make or model"), search_title=tr("Find your vehicle"), search_subtitle=tr("Type a year then a model — for example, 2021 Toyota Corolla:"), search_funcs=[lambda node: node.data.get('display_name', ''), lambda node: node.data.get('search_tags', '')], ) done = partial(self._on_picked, dialog) dialog.on_exit = done gui_app.set_modal_overlay(dialog, callback=done) def _on_picked(self, dialog, res): if res != DialogResult.CONFIRM or not dialog.selection_ref: return when = tr("Applies right away.") if ui_state.is_offroad else \ tr("Applies the next time the device goes offroad.") def confirm(result): if result == DialogResult.CONFIRM and self.selection.force(dialog.selection_ref): self._notify() gui_app.set_modal_overlay(ConfirmDialog(when, tr("Confirm")), callback=confirm) _LEGEND = ( (FingerprintStatus.AUTO, lambda: tr("Detected automatically")), (FingerprintStatus.FORCED, lambda: tr("Chosen manually")), (FingerprintStatus.NONE, lambda: tr("Undetected and unset")), ) class LegendWidget(Widget): def __init__(self, platform_selector: VehiclePicker): super().__init__() self.set_rect(rl.Rectangle(0, 0, 0, 350)) self._selector = platform_selector self._font = gui_app.font(FontWeight.NORMAL) self._bold_font = gui_app.font(FontWeight.BOLD) def _render(self, rect): x = rect.x + 20 y = rect.y + 20 rl.draw_text_ex(self._font, tr("Pin a vehicle to skip auto-detection."), rl.Vector2(x, y), 40, 0, ink.CAPTION) y += 80 rl.draw_text_ex(self._font, tr("Colour key for fingerprint state:"), rl.Vector2(x, y), 40, 0, ink.CAPTION) y += 80 active = self._selector.status for status, label in _LEGEND: font = self._bold_font if status == active else self._font text_color = rl.WHITE if status == active else ink.CAPTION text = f"- {label()}" ts = measure_text_cached(font, text, 40) chip_cy = (y - 7) + ts.y / 2 rl.draw_rectangle_rounded(rl.Rectangle(x, chip_cy - 18, 36, 36), 0.45, 10, STATUS_COLORS[status]) rl.draw_text_ex(font, text, rl.Vector2(x + 56, y - 7), 40, 0, text_color) y += 50 _STATUS_BADGES = { FingerprintStatus.AUTO: lambda: tr("AUTO"), FingerprintStatus.FORCED: lambda: tr("MANUAL"), FingerprintStatus.NONE: lambda: tr("NONE"), } class VehicleLayout(Widget): def __init__(self): super().__init__() self._brand_settings = None self._current_brand = None self._platform_selector = VehiclePicker(self._on_vehicle_changed) self._vehicle_item = IQListItem(title=self._platform_selector.text, action_item=ButtonAction(text=tr("SELECT")), callback=self._platform_selector._on_clicked) self._legend_widget = LegendWidget(self._platform_selector) self._refresh_vehicle_row() self.items = [self._vehicle_item, self._legend_widget] self._scroller = Scroller(self.items, line_separator=True, spacing=0) @staticmethod def get_brand(): bundle = ui_state.params.get("CarPlatformBundle") if bundle: return bundle.get("brand", "") fingerprinted = ui_state.CP and ui_state.CP.carFingerprint != "MOCK" return ui_state.CP.brand if fingerprinted else "" def _refresh_vehicle_row(self): status = self._platform_selector.status self._vehicle_item._title = self._platform_selector.text self._vehicle_item.title_color = rl.WHITE self._vehicle_item.title_badge = (_STATUS_BADGES[status](), STATUS_COLORS[status]) forced = ui_state.params.get("CarPlatformBundle") is not None self._vehicle_item.action_item.set_text(tr("REMOVE") if forced else tr("SELECT")) def _sync_brand_panel(self): brand = self.get_brand() if brand == self._current_brand: return self._current_brand = brand self._brand_settings = brand_settings_for(brand) brand_rows = self._brand_settings.items if self._brand_settings else [] self.items = [self._vehicle_item, self._legend_widget, *brand_rows] self._scroller = Scroller(self.items, line_separator=True, spacing=0) def _on_vehicle_changed(self): self._refresh_vehicle_row() self._sync_brand_panel() def _update_state(self): self._on_vehicle_changed() if self._brand_settings: self._brand_settings.update_settings() self._platform_selector.refresh() def _render(self, rect): self._scroller.render(rect) def show_event(self): self._scroller.show_event()