IQ.Pilot Release Commit @ bec7652
This commit is contained in:
0
iqpilot/selfdrive/ui/mici/layouts/__init__.py
Normal file
0
iqpilot/selfdrive/ui/mici/layouts/__init__.py
Normal file
353
iqpilot/selfdrive/ui/mici/layouts/home.py
Normal file
353
iqpilot/selfdrive/ui/mici/layouts/home.py
Normal file
@@ -0,0 +1,353 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from iqpilot.cereal import log
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from iqpilot.system.ui.widgets.label import gui_label, MiciLabel, UnifiedLabel
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
from iqpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR, MousePos
|
||||
from iqpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.system.ui.text import wrap_text
|
||||
from iqpilot.system.version import training_version, RELEASE_IQ_BRANCHES
|
||||
|
||||
HEAD_BUTTON_FONT_SIZE = 40
|
||||
HOME_PADDING = 8
|
||||
HOME_TITLE_MAX_FONT_SIZE = 72
|
||||
HOME_TITLE_MIN_FONT_SIZE = 36
|
||||
HOME_TITLE_TEXT = "IQ.Pilot"
|
||||
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
|
||||
NETWORK_TYPES = {
|
||||
NetworkType.none: "Offline",
|
||||
NetworkType.wifi: "WiFi",
|
||||
NetworkType.cell2G: "2G",
|
||||
NetworkType.cell3G: "3G",
|
||||
NetworkType.cell4G: "LTE",
|
||||
NetworkType.cell5G: "5G",
|
||||
NetworkType.ethernet: "Ethernet",
|
||||
}
|
||||
|
||||
|
||||
class DeviceStatus(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, 300, 175))
|
||||
self._update_state()
|
||||
self._version_text = self._get_version_text()
|
||||
|
||||
self._do_welcome()
|
||||
|
||||
def _do_welcome(self):
|
||||
ui_state.params.put("CompletedTrainingVersion", training_version)
|
||||
|
||||
def refresh(self):
|
||||
self._update_state()
|
||||
self._version_text = self._get_version_text()
|
||||
|
||||
def _get_version_text(self) -> str:
|
||||
brand = "IQ.Pilot"
|
||||
description = ui_state.params.get("UpdaterCurrentDescription")
|
||||
return f"{brand} {description}" if description else brand
|
||||
|
||||
def _update_state(self):
|
||||
# TODO: refresh function that can be called periodically, not at 60 fps, so we can update version
|
||||
# update system status
|
||||
self._system_status = "SYSTEM READY ✓" if ui_state.panda_type != log.PandaState.PandaType.unknown else "BOOTING UP..."
|
||||
|
||||
# update network status
|
||||
strength = ui_state.sm['deviceState'].networkStrength.raw
|
||||
strength_text = "● " * strength + "○ " * (4 - strength) # ◌ also works
|
||||
network_type = NETWORK_TYPES[ui_state.sm['deviceState'].networkType.raw]
|
||||
self._network_status = f"{network_type} {strength_text}"
|
||||
|
||||
def _render(self, _):
|
||||
# draw status
|
||||
status_rect = rl.Rectangle(self._rect.x, self._rect.y, self._rect.width, 40)
|
||||
gui_label(status_rect, self._system_status, font_size=HEAD_BUTTON_FONT_SIZE, color=DEFAULT_TEXT_COLOR,
|
||||
font_weight=FontWeight.BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
|
||||
# draw network status
|
||||
network_rect = rl.Rectangle(self._rect.x, self._rect.y + 60, self._rect.width, 40)
|
||||
gui_label(network_rect, self._network_status, font_size=40, color=DEFAULT_TEXT_COLOR,
|
||||
font_weight=FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
|
||||
# draw version
|
||||
version_font_size = 30
|
||||
version_rect = rl.Rectangle(self._rect.x, self._rect.y + 140, self._rect.width + 20, 40)
|
||||
wrapped_text = '\n'.join(wrap_text(self._version_text, version_font_size, version_rect.width))
|
||||
gui_label(version_rect, wrapped_text, font_size=version_font_size, color=DEFAULT_TEXT_COLOR,
|
||||
font_weight=FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
|
||||
|
||||
class MiciHomeLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._on_settings_click: Callable | None = None
|
||||
|
||||
self._last_refresh = 0
|
||||
self._mouse_down_t: None | float = None
|
||||
self._did_long_press = False
|
||||
self._is_pressed_prev = False
|
||||
|
||||
self._version_text = None
|
||||
self._experimental_mode = False
|
||||
|
||||
self._settings_txt = gui_app.texture("icons_mici/settings.png", 48, 48)
|
||||
self._experimental_txt = gui_app.texture("icons_mici/experimental_mode_mici.png", 48, 48)
|
||||
self._iqdynamic_txt = gui_app.texture("icons_mici/iqdynamic_mode_mici.png", 48, 48)
|
||||
self._iqstandard_txt = gui_app.texture("icons_mici/iqstandard_mode_mici.png", 48, 48)
|
||||
self._mode_txt = None
|
||||
self._mic_txt = gui_app.texture("icons_mici/microphone.png", 32, 46)
|
||||
|
||||
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
||||
self._net_strength = 0
|
||||
|
||||
self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 50, 44)
|
||||
self._wifi_none_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_none.png", 50, 37)
|
||||
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 50, 37)
|
||||
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 50, 37)
|
||||
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 50, 37)
|
||||
|
||||
self._cell_none_txt = gui_app.texture("icons_mici/settings/network/cell_strength_none.png", 54, 36)
|
||||
self._cell_low_txt = gui_app.texture("icons_mici/settings/network/cell_strength_low.png", 54, 36)
|
||||
self._cell_medium_txt = gui_app.texture("icons_mici/settings/network/cell_strength_medium.png", 54, 36)
|
||||
self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 54, 36)
|
||||
self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 54, 36)
|
||||
self._lte_label = UnifiedLabel("LTE", font_size=22, text_color=rl.Color(255, 255, 255, int(255 * 0.82)),
|
||||
font_weight=FontWeight.BOLD)
|
||||
|
||||
self._openpilot_label = MiciLabel(HOME_TITLE_TEXT, font_size=HOME_TITLE_MAX_FONT_SIZE, color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||
font_weight=FontWeight.SYNCOPATE)
|
||||
self._version_label = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN)
|
||||
self._large_version_label = MiciLabel("", font_size=64, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
self._date_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
self._branch_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, scroll=True)
|
||||
self._version_commit_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY,
|
||||
font_weight=FontWeight.ROMAN, wrap_text=False, scroll=True)
|
||||
|
||||
def show_event(self):
|
||||
self._version_text = self._get_version_text()
|
||||
self._update_network_status(ui_state.sm['deviceState'])
|
||||
self._update_params()
|
||||
|
||||
def _update_params(self):
|
||||
p = ui_state.params
|
||||
self._experimental_mode = p.get_bool("ExperimentalMode")
|
||||
if not p.get_bool("AlphaLongitudinalEnabled"):
|
||||
self._mode_txt = None
|
||||
elif not self._experimental_mode:
|
||||
self._mode_txt = self._iqstandard_txt
|
||||
elif p.get_bool("IQDynamicMode"):
|
||||
self._mode_txt = self._iqdynamic_txt
|
||||
else:
|
||||
self._mode_txt = self._experimental_txt
|
||||
|
||||
def _update_state(self):
|
||||
if self.is_pressed and not self._is_pressed_prev:
|
||||
self._mouse_down_t = time.monotonic()
|
||||
elif not self.is_pressed and self._is_pressed_prev:
|
||||
self._mouse_down_t = None
|
||||
self._did_long_press = False
|
||||
self._is_pressed_prev = self.is_pressed
|
||||
|
||||
if self._mouse_down_t is not None:
|
||||
if time.monotonic() - self._mouse_down_t > 0.5:
|
||||
# long gating for experimental mode - only allow toggle if longitudinal control is available
|
||||
if ui_state.has_longitudinal_control:
|
||||
self._experimental_mode = not self._experimental_mode
|
||||
ui_state.params.put("ExperimentalMode", self._experimental_mode)
|
||||
if not self._experimental_mode:
|
||||
ui_state.params.put_bool("IQDynamicMode", False)
|
||||
self._update_params()
|
||||
self._mouse_down_t = None
|
||||
self._did_long_press = True
|
||||
|
||||
if rl.get_time() - self._last_refresh > 5.0:
|
||||
device_state = ui_state.sm['deviceState']
|
||||
self._update_network_status(device_state)
|
||||
|
||||
# Update version text
|
||||
self._version_text = self._get_version_text()
|
||||
self._last_refresh = rl.get_time()
|
||||
self._update_params()
|
||||
|
||||
def _update_network_status(self, device_state):
|
||||
self._net_type = device_state.networkType
|
||||
strength = device_state.networkStrength
|
||||
self._net_strength = max(0, min(5, strength.raw + 1)) if strength.raw > 0 else 0
|
||||
|
||||
def set_callbacks(self, on_settings: Callable | None = None):
|
||||
self._on_settings_click = on_settings
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if not self._did_long_press:
|
||||
if self._on_settings_click:
|
||||
self._on_settings_click()
|
||||
self._did_long_press = False
|
||||
|
||||
def _get_version_text(self) -> tuple[str, str, str, str] | None:
|
||||
description = ui_state.params.get("UpdaterCurrentDescription")
|
||||
|
||||
if description is not None and len(description) > 0:
|
||||
# Expect "version / branch / commit / date"; be tolerant of other formats
|
||||
try:
|
||||
version, branch, commit, date = description.split(" / ")
|
||||
return version, branch, commit, date
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
def _fit_title_font(self, title_x: float) -> None:
|
||||
font = gui_app.font(FontWeight.SYNCOPATE)
|
||||
viewport_width = min(w for w in (self.rect.width, gui_app.width, rl.get_screen_width()) if w > 0)
|
||||
max_width = max(100, int(viewport_width - title_x - HOME_PADDING - 16))
|
||||
fit_width = max_width * 0.9
|
||||
text_width = measure_text_cached(font, HOME_TITLE_TEXT, HOME_TITLE_MAX_FONT_SIZE).x
|
||||
if text_width <= fit_width:
|
||||
target_size = HOME_TITLE_MAX_FONT_SIZE
|
||||
else:
|
||||
target_size = int(HOME_TITLE_MAX_FONT_SIZE * (fit_width / text_width))
|
||||
target_size = max(HOME_TITLE_MIN_FONT_SIZE, min(HOME_TITLE_MAX_FONT_SIZE, target_size))
|
||||
|
||||
if target_size != self._openpilot_label.font_size:
|
||||
self._openpilot_label.set_font_size(target_size)
|
||||
|
||||
def _render_title_gradient(self, x: float, y: float) -> None:
|
||||
"""Render HOME_TITLE_TEXT with a left-to-right gradient (#7400b8 → #80ffdb).
|
||||
|
||||
Technique: render white text to an offscreen RenderTexture, then draw it
|
||||
tinted by sampling the gradient per-column using draw_texture_pro with a
|
||||
tint. Since draw_texture_pro only supports a single tint color, we use the
|
||||
BLEND_MULTIPLIED trick:
|
||||
1. Draw white text normally onto the framebuffer.
|
||||
2. Draw gradient rect with BLEND_MULTIPLIED on top — this multiplies each
|
||||
existing pixel by the gradient color, turning white text into the gradient
|
||||
while the dark background (near-zero RGB) stays dark.
|
||||
"""
|
||||
font = gui_app.font(FontWeight.SYNCOPATE)
|
||||
font_size = self._openpilot_label.font_size
|
||||
text = HOME_TITLE_TEXT
|
||||
|
||||
text_size = measure_text_cached(font, text, font_size)
|
||||
tw = int(text_size.x) + 4
|
||||
th = int(text_size.y) + 4
|
||||
|
||||
# Step 1: draw white text at full opacity
|
||||
rl.draw_text_ex(font, text, rl.Vector2(x + 2, y + 2), font_size, 0,
|
||||
rl.Color(255, 255, 255, 230))
|
||||
|
||||
# Step 2: multiply gradient over the text region — white → gradient color,
|
||||
# black background → stays black (0 × anything = 0)
|
||||
rl.begin_blend_mode(rl.BlendMode.BLEND_MULTIPLIED)
|
||||
rl.draw_rectangle_gradient_h(
|
||||
int(x), int(y), tw, th,
|
||||
rl.Color(0x80, 0xff, 0xdb, 255), # #80ffdb — left
|
||||
rl.Color(0x74, 0x00, 0xb8, 255), # #7400b8 — right
|
||||
)
|
||||
rl.end_blend_mode()
|
||||
|
||||
def _render(self, _):
|
||||
text_pos = rl.Vector2(self.rect.x - 2 + HOME_PADDING, self.rect.y + HOME_PADDING)
|
||||
self._fit_title_font(text_pos.x)
|
||||
self._render_title_gradient(text_pos.x, text_pos.y)
|
||||
|
||||
if self._version_text is not None:
|
||||
# release branch
|
||||
release_branch = self._version_text[1] in RELEASE_IQ_BRANCHES
|
||||
version_pos = rl.Rectangle(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16, 100, 44)
|
||||
self._version_label.set_text(self._version_text[0])
|
||||
self._version_label.set_position(version_pos.x, version_pos.y)
|
||||
self._version_label.render()
|
||||
|
||||
self._date_label.set_text(" " + self._version_text[3])
|
||||
self._date_label.set_position(version_pos.x + self._version_label.rect.width + 10, version_pos.y)
|
||||
self._date_label.render()
|
||||
|
||||
viewport_right = min(w for w in (self.rect.x + self.rect.width, gui_app.width, rl.get_screen_width()) if w > 0)
|
||||
branch_x = version_pos.x + self._version_label.rect.width + self._date_label.rect.width + 20
|
||||
self._branch_label.set_max_width(max(80, int(viewport_right - branch_x - HOME_PADDING)))
|
||||
self._branch_label.set_text(" " + ("release" if release_branch else self._version_text[1]))
|
||||
self._branch_label.set_position(branch_x, version_pos.y)
|
||||
self._branch_label.render()
|
||||
|
||||
if not release_branch:
|
||||
# 2nd line
|
||||
self._version_commit_label.set_text(self._version_text[2])
|
||||
commit_y = version_pos.y + self._date_label.font_size + 7
|
||||
commit_rect = rl.Rectangle(version_pos.x, commit_y, max(100, viewport_right - version_pos.x - HOME_PADDING), 44)
|
||||
self._version_commit_label.render(commit_rect)
|
||||
|
||||
self._render_bottom_status_bar()
|
||||
|
||||
def _render_bottom_status_bar(self):
|
||||
# ***** Center-aligned bottom section icons *****
|
||||
|
||||
# TODO: refactor repeated icon drawing into a small loop
|
||||
ITEM_SPACING = 18
|
||||
Y_CENTER = 24
|
||||
|
||||
last_x = self.rect.x + HOME_PADDING
|
||||
|
||||
# Draw settings icon in bottom left corner
|
||||
rl.draw_texture(self._settings_txt, int(last_x), int(self._rect.y + self.rect.height - self._settings_txt.height / 2 - Y_CENTER),
|
||||
rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
last_x = last_x + self._settings_txt.width + ITEM_SPACING
|
||||
|
||||
# draw network
|
||||
if self._net_type == NetworkType.wifi:
|
||||
# There is no 1
|
||||
draw_net_txt = {0: self._wifi_none_txt,
|
||||
2: self._wifi_low_txt,
|
||||
3: self._wifi_medium_txt,
|
||||
4: self._wifi_full_txt,
|
||||
5: self._wifi_full_txt}.get(self._net_strength, self._wifi_low_txt)
|
||||
rl.draw_texture(draw_net_txt, int(last_x),
|
||||
int(self._rect.y + self.rect.height - draw_net_txt.height / 2 - Y_CENTER), rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
last_x += draw_net_txt.width + ITEM_SPACING
|
||||
|
||||
elif self._net_type in (NetworkType.cell2G, NetworkType.cell3G, NetworkType.cell4G, NetworkType.cell5G):
|
||||
last_x = self._draw_cellular_cluster(last_x, ITEM_SPACING, Y_CENTER, connected=True)
|
||||
|
||||
else:
|
||||
last_x = self._draw_cellular_cluster(last_x, ITEM_SPACING, Y_CENTER, connected=False)
|
||||
|
||||
if self._mode_txt is not None:
|
||||
rl.draw_texture(self._mode_txt, int(last_x),
|
||||
int(self._rect.y + self.rect.height - self._mode_txt.height / 2 - Y_CENTER), rl.Color(255, 255, 255, 255))
|
||||
last_x += self._mode_txt.width + ITEM_SPACING
|
||||
|
||||
# draw microphone icon when recording audio is enabled
|
||||
if ui_state.recording_audio:
|
||||
rl.draw_texture(self._mic_txt, int(last_x),
|
||||
int(self._rect.y + self.rect.height - self._mic_txt.height / 2 - Y_CENTER), rl.Color(255, 255, 255, 255))
|
||||
last_x += self._mic_txt.width + ITEM_SPACING
|
||||
|
||||
def _draw_cellular_cluster(self, start_x: float, spacing: int, y_center: int, connected: bool) -> float:
|
||||
draw_net_txt = {0: self._cell_none_txt,
|
||||
2: self._cell_low_txt,
|
||||
3: self._cell_medium_txt,
|
||||
4: self._cell_high_txt,
|
||||
5: self._cell_full_txt}.get(self._net_strength, self._cell_none_txt)
|
||||
|
||||
icon_y = int(self._rect.y + self.rect.height - draw_net_txt.height / 2 - y_center)
|
||||
rl.draw_texture(draw_net_txt, int(start_x), icon_y, rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
|
||||
label_x = start_x + draw_net_txt.width + 12
|
||||
label_y = self._rect.y + self.rect.height - 37
|
||||
label_rect = rl.Rectangle(label_x, label_y, 44, 24)
|
||||
self._lte_label.render(label_rect)
|
||||
|
||||
if not connected:
|
||||
slash_start = rl.Vector2(start_x + 8, icon_y + draw_net_txt.height - 3)
|
||||
slash_end = rl.Vector2(start_x + draw_net_txt.width - 2, icon_y + 3)
|
||||
rl.draw_line_ex(slash_start, slash_end, 8, rl.Color(255, 255, 255, int(255 * 0.22)))
|
||||
rl.draw_line_ex(slash_start, slash_end, 5, rl.Color(255, 255, 255, int(255 * 0.82)))
|
||||
|
||||
return label_x + label_rect.width + spacing
|
||||
134
iqpilot/selfdrive/ui/mici/layouts/main.py
Normal file
134
iqpilot/selfdrive/ui/mici/layouts/main.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
import pyray as rl
|
||||
import iqpilot.cereal.messaging as messaging
|
||||
from iqpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout
|
||||
from iqpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts
|
||||
from iqpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
|
||||
from iqpilot.selfdrive.ui.ui_state import device, ui_state
|
||||
from iqpilot.selfdrive.ui.mici.layouts.onboarding import OnboardingWindow
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
from iqpilot.system.ui.widgets.scroller import Scroller
|
||||
from iqpilot.system.ui.lib.application import gui_app
|
||||
from iqpilot.system.ui.lib.multilang import multilang
|
||||
from iqpilot.system.version import training_version
|
||||
|
||||
|
||||
ONROAD_DELAY = 2.5 # seconds
|
||||
|
||||
|
||||
class MiciMainLayout(Widget):
|
||||
"""Root mici layout. Lives at the bottom of the nav stack; settings push on top.
|
||||
|
||||
Keeps the IQ.Pilot custom home + onroad as horizontally-scrolled pages, while
|
||||
the (stock) settings open as a swipe-to-dismiss NavWidget on the nav stack.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._pm = messaging.PubMaster(['bookmarkButton'])
|
||||
|
||||
self._prev_onroad = False
|
||||
self._prev_standstill = False
|
||||
self._onroad_time_delay: float | None = None
|
||||
self._setup = False
|
||||
self._rebuild_settings = False
|
||||
|
||||
self._home_layout = MiciHomeLayout()
|
||||
self._alerts_layout = MiciOffroadAlerts()
|
||||
self._settings_layout = SettingsLayout()
|
||||
self._onroad_layout = AugmentedRoadView(bookmark_callback=self._on_bookmark_clicked)
|
||||
|
||||
for widget in (self._home_layout, self._settings_layout, self._alerts_layout, self._onroad_layout):
|
||||
widget.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
|
||||
self._scroller = Scroller([
|
||||
self._alerts_layout,
|
||||
self._home_layout,
|
||||
self._onroad_layout,
|
||||
], spacing=0, pad_start=0, pad_end=0)
|
||||
self._scroller.set_reset_scroll_at_show(False)
|
||||
|
||||
# Disable scrolling when onroad is interacting with bookmark
|
||||
self._scroller.set_scrolling_enabled(lambda: not self._onroad_layout.is_swiping_left())
|
||||
|
||||
self._setup_callbacks()
|
||||
|
||||
if ui_state.params.get("CompletedTrainingVersion") != training_version:
|
||||
ui_state.params.put("CompletedTrainingVersion", training_version)
|
||||
|
||||
gui_app.add_nav_stack_tick(self._handle_transitions)
|
||||
gui_app.push_widget(self)
|
||||
|
||||
self._onboarding_window = OnboardingWindow()
|
||||
if not self._onboarding_window.completed:
|
||||
gui_app.set_modal_overlay(self._onboarding_window)
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self._home_layout.set_callbacks(on_settings=self._on_settings_clicked)
|
||||
self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
|
||||
device.add_interactive_timeout_callback(self._on_interactive_timeout)
|
||||
multilang.add_change_callback(self._on_language_changed)
|
||||
|
||||
def _on_language_changed(self):
|
||||
self._rebuild_settings = True
|
||||
|
||||
def _scroll_to(self, layout: Widget):
|
||||
self._scroller.scroll_to(int(layout.rect.x), smooth=True)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
|
||||
def _render(self, _):
|
||||
if not self._setup:
|
||||
if self._alerts_layout.active_alerts() > 0:
|
||||
self._scroller.scroll_to(self._alerts_layout.rect.x)
|
||||
else:
|
||||
self._scroller.scroll_to(self._rect.width)
|
||||
self._setup = True
|
||||
|
||||
self._scroller.render(self._rect)
|
||||
|
||||
def _handle_transitions(self):
|
||||
if ui_state.started != self._prev_onroad:
|
||||
self._prev_onroad = ui_state.started
|
||||
|
||||
if ui_state.started:
|
||||
self._onroad_time_delay = rl.get_time()
|
||||
else:
|
||||
self._scroll_to(self._home_layout)
|
||||
|
||||
if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY:
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
self._onroad_time_delay = None
|
||||
|
||||
CS = ui_state.sm["carState"]
|
||||
if not CS.standstill and self._prev_standstill:
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
self._prev_standstill = CS.standstill
|
||||
|
||||
if self._rebuild_settings:
|
||||
self._rebuild_settings = False
|
||||
gui_app.pop_widgets_to(self, instant=True)
|
||||
self._settings_layout = SettingsLayout()
|
||||
self._settings_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
|
||||
def _on_interactive_timeout(self):
|
||||
if ui_state.started:
|
||||
if not ui_state.sm["carState"].standstill:
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
else:
|
||||
gui_app.pop_widgets_to(self, instant=True)
|
||||
self._scroll_to(self._home_layout)
|
||||
|
||||
def _on_settings_clicked(self):
|
||||
gui_app.push_widget(self._settings_layout)
|
||||
|
||||
def _on_bookmark_clicked(self):
|
||||
user_bookmark = messaging.new_message('bookmarkButton')
|
||||
user_bookmark.valid = True
|
||||
self._pm.send('bookmarkButton', user_bookmark)
|
||||
309
iqpilot/selfdrive/ui/mici/layouts/offroad_alerts.py
Normal file
309
iqpilot/selfdrive/ui/mici/layouts/offroad_alerts.py
Normal file
@@ -0,0 +1,309 @@
|
||||
import pyray as rl
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.selfdrive.selfdrived.alertmanager import OFFROAD_ALERTS
|
||||
from iqpilot.system.hardware import HARDWARE
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
from iqpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from iqpilot.system.ui.widgets.scroller import Scroller
|
||||
from iqpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
|
||||
REFRESH_INTERVAL = 5.0 # seconds
|
||||
|
||||
|
||||
class AlertSize(IntEnum):
|
||||
SMALL = 0
|
||||
MEDIUM = 1
|
||||
BIG = 2
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlertData:
|
||||
key: str
|
||||
text: str
|
||||
severity: int
|
||||
visible: bool = False
|
||||
|
||||
|
||||
class AlertItem(Widget):
|
||||
# TODO: click should always go somewhere: home or specific settings pane
|
||||
"""Individual alert item widget with background image and text."""
|
||||
ALERT_WIDTH = 520
|
||||
ALERT_HEIGHT_SMALL = 212
|
||||
ALERT_HEIGHT_MED = 240
|
||||
ALERT_HEIGHT_BIG = 324
|
||||
ALERT_PADDING = 28
|
||||
ICON_SIZE = 64
|
||||
ICON_MARGIN = 12
|
||||
TEXT_COLOR = rl.Color(255, 255, 255, int(255 * 0.9))
|
||||
TITLE_BODY_SPACING = 24
|
||||
|
||||
def __init__(self, alert_data: AlertData):
|
||||
super().__init__()
|
||||
self.alert_data = alert_data
|
||||
|
||||
# Load background textures
|
||||
self._bg_small = gui_app.texture("icons_mici/offroad_alerts/small_alert.png", self.ALERT_WIDTH, self.ALERT_HEIGHT_SMALL)
|
||||
self._bg_small_pressed = gui_app.texture("icons_mici/offroad_alerts/small_alert_pressed.png", self.ALERT_WIDTH, self.ALERT_HEIGHT_SMALL)
|
||||
self._bg_medium = gui_app.texture("icons_mici/offroad_alerts/medium_alert.png", self.ALERT_WIDTH, self.ALERT_HEIGHT_MED)
|
||||
self._bg_medium_pressed = gui_app.texture("icons_mici/offroad_alerts/medium_alert_pressed.png", self.ALERT_WIDTH, self.ALERT_HEIGHT_MED)
|
||||
self._bg_big = gui_app.texture("icons_mici/offroad_alerts/big_alert.png", self.ALERT_WIDTH, self.ALERT_HEIGHT_BIG)
|
||||
self._bg_big_pressed = gui_app.texture("icons_mici/offroad_alerts/big_alert_pressed.png", self.ALERT_WIDTH, self.ALERT_HEIGHT_BIG)
|
||||
|
||||
# Load warning icons
|
||||
self._icon_orange = gui_app.texture("icons_mici/offroad_alerts/orange_warning.png", self.ICON_SIZE, self.ICON_SIZE)
|
||||
self._icon_red = gui_app.texture("icons_mici/offroad_alerts/red_warning.png", self.ICON_SIZE, self.ICON_SIZE)
|
||||
self._icon_green = gui_app.texture("icons_mici/offroad_alerts/green_wheel.png", self.ICON_SIZE, self.ICON_SIZE)
|
||||
|
||||
self._title_label = UnifiedLabel(text="", font_size=32, font_weight=FontWeight.SEMI_BOLD, text_color=self.TEXT_COLOR,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, line_height=0.95)
|
||||
|
||||
self._body_label = UnifiedLabel(text="", font_size=28, font_weight=FontWeight.ROMAN, text_color=self.TEXT_COLOR,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, line_height=0.95)
|
||||
|
||||
self._title_text = ""
|
||||
self._body_text = ""
|
||||
self._alert_size = AlertSize.SMALL
|
||||
|
||||
self._update_content()
|
||||
|
||||
def _split_text(self, text: str) -> tuple[str, str]:
|
||||
"""Split text into title (first sentence) and body (remaining text)."""
|
||||
# Find the end of the first sentence (period, exclamation, or question mark followed by space or end)
|
||||
match = re.search(r'[.!?](?:\s+|$)', text)
|
||||
if match:
|
||||
# Found a sentence boundary - split at the end of the sentence
|
||||
title = text[:match.start()].strip()
|
||||
body = text[match.end():].strip()
|
||||
return title, body
|
||||
else:
|
||||
# No sentence boundary found, return full text as title
|
||||
return "", text
|
||||
|
||||
def _update_content(self):
|
||||
"""Update text and calculate height."""
|
||||
if not self.alert_data.visible or not self.alert_data.text:
|
||||
self.set_visible(False)
|
||||
return
|
||||
|
||||
self.set_visible(True)
|
||||
|
||||
# Split text into title and body
|
||||
self._title_text, self._body_text = self._split_text(self.alert_data.text)
|
||||
|
||||
# Calculate text width (alert width minus padding and icon space on right)
|
||||
title_width = self.ALERT_WIDTH - (self.ALERT_PADDING * 2) - self.ICON_SIZE - self.ICON_MARGIN
|
||||
body_width = self.ALERT_WIDTH - (self.ALERT_PADDING * 2)
|
||||
|
||||
# Update labels
|
||||
self._title_label.set_text(self._title_text)
|
||||
self._body_label.set_text(self._body_text)
|
||||
|
||||
# Calculate content height
|
||||
title_height = self._title_label.get_content_height(title_width) if self._title_text else 0
|
||||
body_height = self._body_label.get_content_height(body_width) if self._body_text else 0
|
||||
spacing = self.TITLE_BODY_SPACING if (self._title_text and self._body_text) else 0
|
||||
total_text_height = title_height + spacing + body_height
|
||||
|
||||
# Determine which background size to use based on content height
|
||||
min_height_with_padding = total_text_height + (self.ALERT_PADDING * 2)
|
||||
if min_height_with_padding > self.ALERT_HEIGHT_MED:
|
||||
self._alert_size = AlertSize.BIG
|
||||
height = self.ALERT_HEIGHT_BIG
|
||||
elif min_height_with_padding > self.ALERT_HEIGHT_SMALL:
|
||||
self._alert_size = AlertSize.MEDIUM
|
||||
height = self.ALERT_HEIGHT_MED
|
||||
else:
|
||||
self._alert_size = AlertSize.SMALL
|
||||
height = self.ALERT_HEIGHT_SMALL
|
||||
|
||||
# Set rect size
|
||||
self.set_rect(rl.Rectangle(0, 0, self.ALERT_WIDTH, height))
|
||||
|
||||
def update_alert_data(self, alert_data: AlertData):
|
||||
"""Update alert data and refresh display."""
|
||||
self.alert_data = alert_data
|
||||
self._update_content()
|
||||
|
||||
def _render(self, _):
|
||||
if not self.alert_data.visible or not self.alert_data.text:
|
||||
return
|
||||
|
||||
# Choose background based on size
|
||||
if self._alert_size == AlertSize.BIG:
|
||||
bg_texture = self._bg_big_pressed if self.is_pressed else self._bg_big
|
||||
elif self._alert_size == AlertSize.MEDIUM:
|
||||
bg_texture = self._bg_medium_pressed if self.is_pressed else self._bg_medium
|
||||
else: # AlertSize.SMALL
|
||||
bg_texture = self._bg_small_pressed if self.is_pressed else self._bg_small
|
||||
|
||||
# Draw background
|
||||
rl.draw_texture(bg_texture, int(self._rect.x), int(self._rect.y), rl.WHITE)
|
||||
|
||||
# Calculate text area (left side, avoiding icon on right)
|
||||
title_width = self.ALERT_WIDTH - (self.ALERT_PADDING * 2) - self.ICON_SIZE - self.ICON_MARGIN
|
||||
body_width = self.ALERT_WIDTH - (self.ALERT_PADDING * 2)
|
||||
text_x = self._rect.x + self.ALERT_PADDING
|
||||
text_y = self._rect.y + self.ALERT_PADDING
|
||||
|
||||
# Draw title label
|
||||
if self._title_text:
|
||||
title_rect = rl.Rectangle(
|
||||
text_x,
|
||||
text_y,
|
||||
title_width,
|
||||
self._title_label.get_content_height(title_width),
|
||||
)
|
||||
self._title_label.render(title_rect)
|
||||
text_y += title_rect.height + self.TITLE_BODY_SPACING
|
||||
|
||||
# Draw body label
|
||||
if self._body_text:
|
||||
body_rect = rl.Rectangle(
|
||||
text_x,
|
||||
text_y,
|
||||
body_width,
|
||||
self._rect.height - text_y + self._rect.y - self.ALERT_PADDING,
|
||||
)
|
||||
self._body_label.render(body_rect)
|
||||
|
||||
# Draw warning icon on the right side
|
||||
# Use green icon for update alerts (severity = -1), red for high severity, orange for low severity
|
||||
if self.alert_data.severity == -1:
|
||||
icon_texture = self._icon_green
|
||||
elif self.alert_data.severity > 0:
|
||||
icon_texture = self._icon_red
|
||||
else:
|
||||
icon_texture = self._icon_orange
|
||||
icon_x = self._rect.x + self.ALERT_WIDTH - self.ALERT_PADDING - self.ICON_SIZE
|
||||
icon_y = self._rect.y + self.ALERT_PADDING
|
||||
rl.draw_texture(icon_texture, int(icon_x), int(icon_y), rl.WHITE)
|
||||
|
||||
|
||||
class MiciOffroadAlerts(Widget):
|
||||
"""Offroad alerts layout with vertical scrolling."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.params = Params()
|
||||
self.sorted_alerts: list[AlertData] = []
|
||||
self.alert_items: list[AlertItem] = []
|
||||
self._last_refresh = 0.0
|
||||
|
||||
# Create vertical scroller
|
||||
self._scroller = Scroller([], horizontal=False, spacing=12, pad_start=0, pad_end=0, snap_items=False)
|
||||
|
||||
# Create empty state label
|
||||
self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
|
||||
# Build initial alert list
|
||||
self._build_alerts()
|
||||
|
||||
def active_alerts(self) -> int:
|
||||
return sum(alert.visible for alert in self.sorted_alerts)
|
||||
|
||||
def scrolling(self):
|
||||
return self._scroller.scroll_panel.is_touch_valid()
|
||||
|
||||
def _build_alerts(self):
|
||||
"""Build sorted list of alerts from OFFROAD_ALERTS."""
|
||||
self.sorted_alerts = []
|
||||
|
||||
# Add UpdateAvailable alert at the top (severity = -1 to indicate special handling)
|
||||
update_alert_data = AlertData(key="UpdateAvailable", text="", severity=-1)
|
||||
self.sorted_alerts.append(update_alert_data)
|
||||
update_alert_item = AlertItem(update_alert_data)
|
||||
update_alert_item.set_click_callback(lambda: HARDWARE.reboot())
|
||||
self.alert_items.append(update_alert_item)
|
||||
self._scroller.add_widget(update_alert_item)
|
||||
|
||||
# Add regular alerts sorted by severity
|
||||
for key, config in sorted(OFFROAD_ALERTS.items(), key=lambda x: x[1].get("severity", 0), reverse=True):
|
||||
severity = config.get("severity", 0)
|
||||
alert_data = AlertData(key=key, text="", severity=severity)
|
||||
self.sorted_alerts.append(alert_data)
|
||||
|
||||
# Create alert item widget
|
||||
alert_item = AlertItem(alert_data)
|
||||
self.alert_items.append(alert_item)
|
||||
self._scroller.add_widget(alert_item)
|
||||
|
||||
def refresh(self) -> int:
|
||||
"""Refresh alerts from params and return active count."""
|
||||
active_count = 0
|
||||
|
||||
# Handle UpdateAvailable alert specially
|
||||
update_available = self.params.get_bool("UpdateAvailable")
|
||||
update_alert_data = next((alert_data for alert_data in self.sorted_alerts if alert_data.key == "UpdateAvailable"), None)
|
||||
|
||||
if update_alert_data:
|
||||
if update_available:
|
||||
version_string = ""
|
||||
|
||||
# Get new version description and parse version and date
|
||||
new_desc = self.params.get("UpdaterNewDescription") or ""
|
||||
if new_desc:
|
||||
# format: "version / branch / commit / date"
|
||||
parts = new_desc.split(" / ")
|
||||
if len(parts) > 3:
|
||||
version, date = parts[0], parts[3]
|
||||
version_string = f"\nIQ.Pilot {version}, {date}\n"
|
||||
|
||||
update_alert_data.text = f"Update available {version_string}. Click to update."
|
||||
update_alert_data.visible = True
|
||||
active_count += 1
|
||||
else:
|
||||
update_alert_data.text = ""
|
||||
update_alert_data.visible = False
|
||||
|
||||
# Handle regular alerts
|
||||
for alert_data in self.sorted_alerts:
|
||||
if alert_data.key == "UpdateAvailable":
|
||||
continue # Skip, already handled above
|
||||
|
||||
text = ""
|
||||
alert_json = self.params.get(alert_data.key)
|
||||
|
||||
if alert_json:
|
||||
text = alert_json.get("text", "").replace("%1", alert_json.get("extra", ""))
|
||||
|
||||
alert_data.text = text
|
||||
alert_data.visible = bool(text)
|
||||
|
||||
if alert_data.visible:
|
||||
active_count += 1
|
||||
|
||||
# Update alert items (they reference the same alert_data objects)
|
||||
for alert_item in self.alert_items:
|
||||
alert_item.update_alert_data(alert_item.alert_data)
|
||||
|
||||
return active_count
|
||||
|
||||
def show_event(self):
|
||||
"""Reset scroll position when shown and refresh alerts."""
|
||||
self._scroller.show_event()
|
||||
self._last_refresh = time.monotonic()
|
||||
self.refresh()
|
||||
|
||||
def _update_state(self):
|
||||
"""Periodically refresh alerts."""
|
||||
# Refresh alerts periodically, not every frame
|
||||
current_time = time.monotonic()
|
||||
if current_time - self._last_refresh >= REFRESH_INTERVAL:
|
||||
self.refresh()
|
||||
self._last_refresh = current_time
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
"""Render the alerts scroller or empty state."""
|
||||
if self.active_alerts() == 0:
|
||||
self._empty_label.render(rect)
|
||||
else:
|
||||
self._scroller.render(rect)
|
||||
533
iqpilot/selfdrive/ui/mici/layouts/onboarding.py
Normal file
533
iqpilot/selfdrive/ui/mici/layouts/onboarding.py
Normal file
@@ -0,0 +1,533 @@
|
||||
from enum import IntEnum
|
||||
|
||||
import weakref
|
||||
import math
|
||||
import os
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from iqpilot.common.filter_simple import FirstOrderFilter
|
||||
from iqpilot.system.hardware import HARDWARE, PC
|
||||
from iqpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
from iqpilot.system.ui.widgets.button import SmallButton, SmallCircleIconButton
|
||||
from iqpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from iqpilot.system.ui.widgets.slider import SmallSlider
|
||||
from iqpilot.system.ui.mici_setup import TermsHeader, TermsPage as SetupTermsPage
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from iqpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
|
||||
from iqpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
|
||||
from iqpilot.system.ui.widgets.label import gui_label
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
from iqpilot.system.version import terms_version, training_version
|
||||
|
||||
|
||||
class OnboardingState(IntEnum):
|
||||
TERMS = 0
|
||||
ONBOARDING = 1
|
||||
DECLINE = 2
|
||||
|
||||
|
||||
class DriverCameraSetupDialog(DriverCameraDialog):
|
||||
def __init__(self):
|
||||
super().__init__(no_escape=True)
|
||||
self.driver_state_renderer = DriverStateRenderer(inset=True)
|
||||
self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 120, 120))
|
||||
self.driver_state_renderer.load_icons()
|
||||
self.driver_state_renderer.set_force_active(True)
|
||||
|
||||
def _render(self, rect):
|
||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||
self._camera_view._render(rect)
|
||||
|
||||
if not self._camera_view.frame:
|
||||
gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
rl.end_scissor_mode()
|
||||
return -1
|
||||
|
||||
# Position dmoji on opposite side from driver
|
||||
is_rhd = self.driver_state_renderer.is_rhd
|
||||
self.driver_state_renderer.set_position(
|
||||
rect.x + 8 if is_rhd else rect.x + rect.width - self.driver_state_renderer.rect.width - 8,
|
||||
rect.y + 8,
|
||||
)
|
||||
self.driver_state_renderer.render()
|
||||
|
||||
self._draw_face_detection(rect)
|
||||
|
||||
rl.end_scissor_mode()
|
||||
return -1
|
||||
|
||||
|
||||
class TrainingGuidePreDMTutorial(SetupTermsPage):
|
||||
def __init__(self, continue_callback):
|
||||
super().__init__(continue_callback, continue_text=tr("continue"))
|
||||
self._title_header = TermsHeader("driver monitoring setup", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
|
||||
|
||||
self._dm_label = UnifiedLabel("Next, we'll ensure comma four is mounted properly.\n\nIf it does not have a clear view of the driver, " +
|
||||
"unplug and remount before continuing.", 42,
|
||||
FontWeight.ROMAN)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
# Get driver monitoring model ready for next step
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", True)
|
||||
|
||||
@property
|
||||
def _content_height(self):
|
||||
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
|
||||
|
||||
def _render_content(self, scroll_offset):
|
||||
self._title_header.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._rect.y + 16 + scroll_offset,
|
||||
self._title_header.rect.width,
|
||||
self._title_header.rect.height,
|
||||
))
|
||||
|
||||
self._dm_label.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._title_header.rect.y + self._title_header.rect.height + 16,
|
||||
self._rect.width - 32,
|
||||
self._dm_label.get_content_height(int(self._rect.width - 32)),
|
||||
))
|
||||
|
||||
|
||||
class DMBadFaceDetected(SetupTermsPage):
|
||||
def __init__(self, continue_callback, back_callback):
|
||||
super().__init__(continue_callback, back_callback, continue_text=tr("power off"))
|
||||
self._title_header = TermsHeader("make sure comma four can see your face", gui_app.texture("icons_mici/setup/orange_dm.png", 60, 60))
|
||||
self._dm_label = UnifiedLabel(tr("Re-mount if your face is occluded or driver monitoring has difficulty tracking your face."), 42, FontWeight.ROMAN)
|
||||
|
||||
@property
|
||||
def _content_height(self):
|
||||
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
|
||||
|
||||
def _render_content(self, scroll_offset):
|
||||
self._title_header.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._rect.y + 16 + scroll_offset,
|
||||
self._title_header.rect.width,
|
||||
self._title_header.rect.height,
|
||||
))
|
||||
|
||||
self._dm_label.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._title_header.rect.y + self._title_header.rect.height + 16,
|
||||
self._rect.width - 32,
|
||||
self._dm_label.get_content_height(int(self._rect.width - 32)),
|
||||
))
|
||||
|
||||
|
||||
class TrainingGuideDMTutorial(Widget):
|
||||
PROGRESS_DURATION = 4
|
||||
LOOKING_THRESHOLD_DEG = 30.0
|
||||
NO_CAMERA_BYPASS_DELAY_SEC = 2.0
|
||||
|
||||
def __init__(self, continue_callback):
|
||||
super().__init__()
|
||||
self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 28, 48))
|
||||
self._back_button.set_click_callback(self._show_bad_face_page)
|
||||
self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 42, 42))
|
||||
|
||||
# Wrap the continue callback to restore settings
|
||||
def wrapped_continue_callback():
|
||||
device.set_offroad_brightness(None)
|
||||
continue_callback()
|
||||
|
||||
self._good_button.set_click_callback(wrapped_continue_callback)
|
||||
self._good_button.set_enabled(False)
|
||||
|
||||
self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps)
|
||||
self._dialog = DriverCameraSetupDialog()
|
||||
self._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, self._hide_bad_face_page)
|
||||
self._should_show_bad_face_page = False
|
||||
self._no_camera_elapsed_sec = 0.0
|
||||
self._allow_no_camera_bypass = PC or os.getenv("IQPILOT_ALLOW_DM_NO_CAMERA", "0") == "1"
|
||||
|
||||
# Disable driver monitoring model when device times out for inactivity
|
||||
def inactivity_callback():
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||
|
||||
device.add_interactive_timeout_callback(inactivity_callback)
|
||||
|
||||
def _show_bad_face_page(self):
|
||||
self._bad_face_page.show_event()
|
||||
self.hide_event()
|
||||
self._should_show_bad_face_page = True
|
||||
|
||||
def _hide_bad_face_page(self):
|
||||
self._bad_face_page.hide_event()
|
||||
self.show_event()
|
||||
self._should_show_bad_face_page = False
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._dialog.show_event()
|
||||
self._progress.x = 0.0
|
||||
self._no_camera_elapsed_sec = 0.0
|
||||
|
||||
device.set_offroad_brightness(100)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if device.awake and not ui_state.params.get_bool("IsDriverViewEnabled"):
|
||||
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", True)
|
||||
|
||||
has_camera_frame = self._dialog._camera_view.frame is not None
|
||||
if has_camera_frame:
|
||||
self._no_camera_elapsed_sec = 0.0
|
||||
else:
|
||||
self._no_camera_elapsed_sec += 1.0 / gui_app.target_fps
|
||||
|
||||
# Dev-mode fallback: allow progressing onboarding even when no camera frames are available.
|
||||
if self._allow_no_camera_bypass and not has_camera_frame:
|
||||
self._good_button.set_enabled(self._no_camera_elapsed_sec >= self.NO_CAMERA_BYPASS_DELAY_SEC)
|
||||
return
|
||||
|
||||
sm = ui_state.sm
|
||||
if sm.recv_frame.get("driverMonitoringState", 0) == 0:
|
||||
self._good_button.set_enabled(False)
|
||||
return
|
||||
|
||||
dm_state = sm["driverMonitoringState"]
|
||||
driver_data = self._dialog.driver_state_renderer.get_driver_data()
|
||||
|
||||
if len(driver_data.faceOrientation) == 3:
|
||||
pitch, yaw, _ = driver_data.faceOrientation
|
||||
looking_center = abs(math.degrees(pitch)) < self.LOOKING_THRESHOLD_DEG and abs(math.degrees(yaw)) < self.LOOKING_THRESHOLD_DEG
|
||||
else:
|
||||
looking_center = False
|
||||
|
||||
# stay at 100% once reached
|
||||
if (dm_state.faceDetected and looking_center) or self._progress.x > 0.99:
|
||||
slow = self._progress.x < 0.25
|
||||
duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION
|
||||
self._progress.x += 1.0 / (duration * gui_app.target_fps)
|
||||
self._progress.x = min(1.0, self._progress.x)
|
||||
else:
|
||||
self._progress.update(0.0)
|
||||
|
||||
self._good_button.set_enabled(self._progress.x >= 0.999)
|
||||
|
||||
def _render(self, _):
|
||||
if self._should_show_bad_face_page:
|
||||
return self._bad_face_page.render(self._rect)
|
||||
|
||||
self._dialog.render(self._rect)
|
||||
|
||||
rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 80),
|
||||
int(self._rect.width), 80, rl.BLANK, rl.BLACK)
|
||||
|
||||
# draw white ring around dm icon to indicate progress
|
||||
ring_thickness = 8
|
||||
|
||||
# DM icon is 120x120, positioned on opposite side from driver
|
||||
dm_size = 120
|
||||
is_rhd = self._dialog.driver_state_renderer._is_rhd
|
||||
dm_center_x = (self._rect.x + dm_size / 2 + 8) if is_rhd else (self._rect.x + self._rect.width - dm_size / 2 - 8)
|
||||
dm_center_y = self._rect.y + dm_size / 2 + 8
|
||||
icon_edge_radius = dm_size / 2
|
||||
outer_radius = icon_edge_radius + 1 # 2px outward from icon edge
|
||||
inner_radius = outer_radius - ring_thickness # Inset by ring_thickness
|
||||
start_angle = 90.0 # Start from bottom
|
||||
end_angle = start_angle + self._progress.x * 360.0 # Clockwise
|
||||
|
||||
# Fade in alpha
|
||||
current_angle = end_angle - start_angle
|
||||
alpha = int(np.interp(current_angle, [0.0, 45.0], [0, 255]))
|
||||
|
||||
# White to green
|
||||
color_t = np.clip(np.interp(current_angle, [45.0, 360.0], [0.0, 1.0]), 0.0, 1.0)
|
||||
r = int(np.interp(color_t, [0.0, 1.0], [255, 0]))
|
||||
g = int(np.interp(color_t, [0.0, 1.0], [255, 255]))
|
||||
b = int(np.interp(color_t, [0.0, 1.0], [255, 64]))
|
||||
ring_color = rl.Color(r, g, b, alpha)
|
||||
|
||||
rl.draw_ring(
|
||||
rl.Vector2(dm_center_x, dm_center_y),
|
||||
inner_radius,
|
||||
outer_radius,
|
||||
start_angle,
|
||||
end_angle,
|
||||
36,
|
||||
ring_color,
|
||||
)
|
||||
|
||||
has_camera_frame = self._dialog._camera_view.frame is not None
|
||||
show_no_camera_bypass = self._allow_no_camera_bypass and not has_camera_frame
|
||||
if has_camera_frame or show_no_camera_bypass:
|
||||
self._back_button.render(rl.Rectangle(
|
||||
self._rect.x + 8,
|
||||
self._rect.y + self._rect.height - self._back_button.rect.height,
|
||||
self._back_button.rect.width,
|
||||
self._back_button.rect.height,
|
||||
))
|
||||
|
||||
self._good_button.render(rl.Rectangle(
|
||||
self._rect.x + self._rect.width - self._good_button.rect.width - 8,
|
||||
self._rect.y + self._rect.height - self._good_button.rect.height,
|
||||
self._good_button.rect.width,
|
||||
self._good_button.rect.height,
|
||||
))
|
||||
|
||||
if show_no_camera_bypass:
|
||||
gui_label(
|
||||
rl.Rectangle(self._rect.x + 20, self._rect.y + self._rect.height - 140, self._rect.width - 40, 60),
|
||||
tr("No camera detected in dev mode. Tap check to continue."),
|
||||
font_size=34,
|
||||
font_weight=FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
)
|
||||
|
||||
# rounded border
|
||||
rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK)
|
||||
|
||||
|
||||
class TrainingGuideRecordFront(SetupTermsPage):
|
||||
def __init__(self, continue_callback):
|
||||
def on_back():
|
||||
ui_state.params.put_bool("RecordFront", False)
|
||||
continue_callback()
|
||||
|
||||
def on_continue():
|
||||
ui_state.params.put_bool("RecordFront", True)
|
||||
continue_callback()
|
||||
|
||||
super().__init__(on_continue, back_callback=on_back, back_text=tr("no"), continue_text=tr("yes"))
|
||||
self._title_header = TermsHeader("improve driver monitoring", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
|
||||
|
||||
self._dm_label = UnifiedLabel(tr("Do you want to upload driver camera data?"), 42,
|
||||
FontWeight.ROMAN)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
# Disable driver monitoring model after last step
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||
|
||||
@property
|
||||
def _content_height(self):
|
||||
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
|
||||
|
||||
def _render_content(self, scroll_offset):
|
||||
self._title_header.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._rect.y + 16 + scroll_offset,
|
||||
self._title_header.rect.width,
|
||||
self._title_header.rect.height,
|
||||
))
|
||||
|
||||
self._dm_label.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._title_header.rect.y + self._title_header.rect.height + 16,
|
||||
self._rect.width - 32,
|
||||
self._dm_label.get_content_height(int(self._rect.width - 32)),
|
||||
))
|
||||
|
||||
|
||||
class TrainingGuideAttentionNotice(SetupTermsPage):
|
||||
def __init__(self, continue_callback):
|
||||
super().__init__(continue_callback, continue_text=tr("continue"))
|
||||
self._title_header = TermsHeader("driver assistance", gui_app.texture("icons_mici/setup/warning.png", 60, 60))
|
||||
self._warning_label = UnifiedLabel("1. IQ.Pilot is a driver assistance system.\n\n" +
|
||||
"2. You must pay attention at all times.\n\n" +
|
||||
"3. You must be ready to take over at any time.\n\n" +
|
||||
"4. You are fully responsible for driving the car.", 42,
|
||||
FontWeight.ROMAN)
|
||||
|
||||
@property
|
||||
def _content_height(self):
|
||||
return self._warning_label.rect.y + self._warning_label.rect.height - self._scroll_panel.get_offset()
|
||||
|
||||
def _render_content(self, scroll_offset):
|
||||
self._title_header.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._rect.y + 16 + scroll_offset,
|
||||
self._title_header.rect.width,
|
||||
self._title_header.rect.height,
|
||||
))
|
||||
|
||||
self._warning_label.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._title_header.rect.y + self._title_header.rect.height + 16,
|
||||
self._rect.width - 32,
|
||||
self._warning_label.get_content_height(int(self._rect.width - 32)),
|
||||
))
|
||||
|
||||
|
||||
class TrainingGuide(Widget):
|
||||
def __init__(self, completed_callback=None):
|
||||
super().__init__()
|
||||
self._completed_callback = completed_callback
|
||||
self._step = 0
|
||||
|
||||
self_ref = weakref.ref(self)
|
||||
|
||||
def on_continue():
|
||||
if obj := self_ref():
|
||||
obj._advance_step()
|
||||
|
||||
self._steps = [
|
||||
TrainingGuideAttentionNotice(continue_callback=on_continue),
|
||||
TrainingGuidePreDMTutorial(continue_callback=on_continue),
|
||||
TrainingGuideDMTutorial(continue_callback=on_continue),
|
||||
TrainingGuideRecordFront(continue_callback=on_continue),
|
||||
]
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
device.set_override_interactive_timeout(300)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
device.set_override_interactive_timeout(None)
|
||||
|
||||
def _advance_step(self):
|
||||
if self._step < len(self._steps) - 1:
|
||||
self._step += 1
|
||||
self._steps[self._step].show_event()
|
||||
else:
|
||||
self._step = 0
|
||||
if self._completed_callback:
|
||||
self._completed_callback()
|
||||
|
||||
def _render(self, _):
|
||||
if self._step < len(self._steps):
|
||||
self._steps[self._step].render(self._rect)
|
||||
return -1
|
||||
|
||||
|
||||
class DeclinePage(Widget):
|
||||
def __init__(self, back_callback=None):
|
||||
super().__init__()
|
||||
self._uninstall_slider = SmallSlider(tr("uninstall IQ.Pilot"), self._on_uninstall)
|
||||
|
||||
self._back_button = SmallButton(tr("back"))
|
||||
self._back_button.set_click_callback(back_callback)
|
||||
|
||||
self._warning_header = TermsHeader("you must accept the\nterms to use IQ.Pilot",
|
||||
gui_app.texture("icons_mici/setup/red_warning.png", 66, 60))
|
||||
|
||||
def _on_uninstall(self):
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
gui_app.request_close()
|
||||
|
||||
def _render(self, _):
|
||||
self._warning_header.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._rect.y + 16,
|
||||
self._warning_header.rect.width,
|
||||
self._warning_header.rect.height,
|
||||
))
|
||||
|
||||
self._back_button.set_opacity(1 - self._uninstall_slider.slider_percentage)
|
||||
self._back_button.render(rl.Rectangle(
|
||||
self._rect.x + 8,
|
||||
self._rect.y + self._rect.height - self._back_button.rect.height,
|
||||
self._back_button.rect.width,
|
||||
self._back_button.rect.height,
|
||||
))
|
||||
|
||||
self._uninstall_slider.render(rl.Rectangle(
|
||||
self._rect.x + self._rect.width - self._uninstall_slider.rect.width,
|
||||
self._rect.y + self._rect.height - self._uninstall_slider.rect.height,
|
||||
self._uninstall_slider.rect.width,
|
||||
self._uninstall_slider.rect.height,
|
||||
))
|
||||
|
||||
|
||||
class TermsPage(SetupTermsPage):
|
||||
def __init__(self, on_accept=None, on_decline=None):
|
||||
super().__init__(on_accept, on_decline, tr("decline"))
|
||||
|
||||
info_txt = gui_app.texture("icons_mici/setup/green_info.png", 60, 60)
|
||||
self._title_header = TermsHeader("terms of service", info_txt)
|
||||
|
||||
self._terms_label = UnifiedLabel("You must accept the Terms of Service to use IQ.Pilot. " +
|
||||
"Read the latest terms before continuing at https://iqlvbs.com/tos", 36,
|
||||
FontWeight.ROMAN)
|
||||
|
||||
@property
|
||||
def _content_height(self):
|
||||
return self._terms_label.rect.y + self._terms_label.rect.height - self._scroll_panel.get_offset()
|
||||
|
||||
def _render_content(self, scroll_offset):
|
||||
self._title_header.set_position(self._rect.x + 16, self._rect.y + 12 + scroll_offset)
|
||||
self._title_header.render()
|
||||
|
||||
self._terms_label.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._title_header.rect.y + self._title_header.rect.height + self.ITEM_SPACING,
|
||||
self._rect.width - 100,
|
||||
self._terms_label.get_content_height(int(self._rect.width - 100)),
|
||||
))
|
||||
|
||||
|
||||
class OnboardingWindow(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._accepted_terms: bool = ui_state.params.get("HasAcceptedTerms") == terms_version
|
||||
self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == training_version
|
||||
|
||||
self._state = OnboardingState.TERMS if not self._accepted_terms else OnboardingState.ONBOARDING
|
||||
|
||||
self.set_rect(rl.Rectangle(0, 0, 458, gui_app.height))
|
||||
|
||||
# Windows
|
||||
self._terms = TermsPage(on_accept=self._on_terms_accepted, on_decline=self._on_terms_declined)
|
||||
self._training_guide = TrainingGuide(completed_callback=self._on_completed_training)
|
||||
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
|
||||
|
||||
if not self._accepted_terms:
|
||||
self._state = OnboardingState.TERMS
|
||||
elif not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
device.set_override_interactive_timeout(300)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
device.set_override_interactive_timeout(None)
|
||||
|
||||
@property
|
||||
def completed(self) -> bool:
|
||||
return self._accepted_terms and self._training_done
|
||||
|
||||
def _on_terms_declined(self):
|
||||
self._state = OnboardingState.DECLINE
|
||||
|
||||
def _on_decline_back(self):
|
||||
self._state = OnboardingState.TERMS
|
||||
|
||||
def close(self):
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
def _on_terms_accepted(self):
|
||||
ui_state.params.put("HasAcceptedTerms", terms_version)
|
||||
if not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
self.close()
|
||||
|
||||
def _on_completed_training(self):
|
||||
ui_state.params.put("CompletedTrainingVersion", training_version)
|
||||
self.close()
|
||||
|
||||
def _render(self, _):
|
||||
# opaque full-screen background so the home/onroad underneath doesn't show through
|
||||
rl.draw_rectangle_rec(rl.Rectangle(0, 0, gui_app.width, gui_app.height), rl.BLACK)
|
||||
if self._state == OnboardingState.TERMS:
|
||||
self._terms.render(self._rect)
|
||||
elif self._state == OnboardingState.ONBOARDING:
|
||||
if not self._training_done:
|
||||
self._training_guide.render(self._rect)
|
||||
else:
|
||||
self.close()
|
||||
elif self._state == OnboardingState.DECLINE:
|
||||
self._decline_page.render(self._rect)
|
||||
return -1
|
||||
104
iqpilot/selfdrive/ui/mici/layouts/settings/cruise.py
Normal file
104
iqpilot/selfdrive/ui/mici/layouts/settings/cruise.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, BigParamControl
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.iq_widgets import MappedParamToggle, IQModeSelector, SafeParamControl
|
||||
from iqpilot.system.ui.lib.application import gui_app
|
||||
from iqpilot.system.ui.widgets.scroller import NavScroller
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
|
||||
FOLLOW_DISTANCE_VALUES = [0, 1, 2, 3]
|
||||
|
||||
MS_TO_MPH = 2.23694
|
||||
_SPEED_MPH = [10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80]
|
||||
_SPEED_OPTIONS = [f"{s} mph" for s in _SPEED_MPH]
|
||||
_SPEED_VALUES = [round(s / MS_TO_MPH, 2) for s in _SPEED_MPH]
|
||||
|
||||
_LEAD_SPEED_MPH = [10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85]
|
||||
_LEAD_SPEED_OPTIONS = [f"{s} mph" for s in _LEAD_SPEED_MPH]
|
||||
_LEAD_SPEED_VALUES = [round(s / MS_TO_MPH, 2) for s in _LEAD_SPEED_MPH]
|
||||
|
||||
_STOP_TIME_OPTIONS = ["1.0s", "1.5s", "2.0s", "2.5s", "3.0s", "3.5s", "4.0s", "4.5s", "5.0s", "5.5s", "6.0s"]
|
||||
_STOP_TIME_VALUES = [1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0]
|
||||
|
||||
_LOOKAHEAD_OPTIONS = ["1.0s", "2.0s", "3.0s", "4.0s", "5.0s", "6.0s", "7.0s", "8.0s", "9.0s", "10.0s"]
|
||||
_LOOKAHEAD_VALUES = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
|
||||
|
||||
|
||||
class DynamicSettingsPanel(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._items = [
|
||||
BigParamControl(tr("IQ.Dynamic Curves"), "IQDynamicConditionalCurves"),
|
||||
BigParamControl(tr("IQ.Dynamic Slower Lead"), "IQDynamicConditionalSlowerLead"),
|
||||
BigParamControl(tr("IQ.Dynamic Stopped Lead"), "IQDynamicConditionalStoppedLead"),
|
||||
BigParamControl(tr("IQ.Dynamic Model Stops"), "IQDynamicConditionalModelStops"),
|
||||
BigParamControl(tr("IQ.Dynamic SLC Fallback"), "IQDynamicConditionalSLCFallback"),
|
||||
MappedParamToggle(tr("IQ.Dynamic Low Speed"), "IQDynamicConditionalSpeed", _SPEED_OPTIONS, _SPEED_VALUES),
|
||||
MappedParamToggle(tr("IQ.Dynamic Lead Speed"), "IQDynamicConditionalLeadSpeed", _LEAD_SPEED_OPTIONS, _LEAD_SPEED_VALUES),
|
||||
MappedParamToggle(tr("Model Stop Time"), "IQDynamicModelStopTime", _STOP_TIME_OPTIONS, _STOP_TIME_VALUES),
|
||||
BigParamControl(tr("IQ Force Stops"), "IQForceStops"),
|
||||
]
|
||||
self._scroller.add_widgets(self._items)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
for w in self._items:
|
||||
w.refresh()
|
||||
|
||||
|
||||
class SlcSettingsPanel(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._items = [
|
||||
MappedParamToggle(tr("SLC Policy"), "SLCPolicy", [tr("map only"), tr("map priority"), tr("combined")], [0, 1, 2]),
|
||||
MappedParamToggle(tr("SLC Override"), "SLCOverrideMethod", [tr("manual"), tr("set speed")], [0, 1]),
|
||||
BigParamControl(tr("SLC Confirm Higher"), "SpeedLimitConfirmationHigher"),
|
||||
BigParamControl(tr("SLC Confirm Lower"), "SpeedLimitConfirmationLower"),
|
||||
BigParamControl(tr("SLC Auto Confirm"), "SLCAutoConfirm"),
|
||||
BigParamControl(tr("SLC Fallback IQ.Pilot"), "SLCFallbackExperimentalMode"),
|
||||
BigParamControl(tr("SLC Online Filler"), "SLCOnlineFiller"),
|
||||
MappedParamToggle(tr("Lookahead Higher"), "MapSpeedLookaheadHigher", _LOOKAHEAD_OPTIONS, _LOOKAHEAD_VALUES),
|
||||
MappedParamToggle(tr("Lookahead Lower"), "MapSpeedLookaheadLower", _LOOKAHEAD_OPTIONS, _LOOKAHEAD_VALUES),
|
||||
]
|
||||
self._scroller.add_widgets(self._items)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
for w in self._items:
|
||||
w.refresh()
|
||||
|
||||
|
||||
class CruiseLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._dynamic_panel = DynamicSettingsPanel()
|
||||
self._slc_panel = SlcSettingsPanel()
|
||||
|
||||
self._mode = IQModeSelector()
|
||||
self._dynamic_settings = BigButton(tr("iq.dynamic settings"))
|
||||
self._dynamic_settings.set_click_callback(lambda: gui_app.push_widget(self._dynamic_panel))
|
||||
self._dynamic_settings.set_visible(self._mode.is_dynamic)
|
||||
self._follow_dist = MappedParamToggle(tr("Follow Distance"), "LongitudinalPersonality",
|
||||
[tr("aggressive"), tr("standard"), tr("relaxed"), tr("stock")], FOLLOW_DISTANCE_VALUES)
|
||||
self._speed_limit = MappedParamToggle(tr("Speed Limit"), "IQSpeedAssistMode",
|
||||
[tr("off"), tr("info"), tr("warning"), tr("control")])
|
||||
self._slc_settings = BigButton(tr("speed limit settings"))
|
||||
self._slc_settings.set_click_callback(lambda: gui_app.push_widget(self._slc_panel))
|
||||
self._new_lead_mpc = SafeParamControl(tr("Experimental Lead MPC"), "newLeadMpc", default_on=True)
|
||||
|
||||
self._main = [self._mode, self._dynamic_settings, self._follow_dist, self._speed_limit,
|
||||
self._new_lead_mpc, self._slc_settings]
|
||||
self._scroller.add_widgets(self._main)
|
||||
|
||||
def _refresh(self):
|
||||
self._mode.refresh()
|
||||
self._follow_dist.refresh()
|
||||
self._speed_limit.refresh()
|
||||
self._new_lead_mpc.refresh()
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._refresh()
|
||||
38
iqpilot/selfdrive/ui/mici/layouts/settings/dashcam.py
Normal file
38
iqpilot/selfdrive/ui/mici/layouts/settings/dashcam.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from iqpilot.system.ui.widgets.scroller import NavScroller
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigParamControl
|
||||
from iqpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
|
||||
|
||||
class DashcamLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._dashcam = BigParamControl(tr("enable dashcam"), "DashcamEnabled", toggle_callback=restart_needed_callback)
|
||||
self._record_front = BigParamControl(tr("record driver camera"), "RecordFront", toggle_callback=restart_needed_callback)
|
||||
self._record_audio = BigParamControl(tr("record microphone audio"), "RecordAudio", toggle_callback=restart_needed_callback)
|
||||
|
||||
self._scroller.add_widgets([self._dashcam, self._record_front, self._record_audio])
|
||||
|
||||
self._refresh_toggles = (
|
||||
("DashcamEnabled", self._dashcam),
|
||||
("RecordFront", self._record_front),
|
||||
("RecordAudio", self._record_audio),
|
||||
)
|
||||
|
||||
self._record_front.set_enabled(False if ui_state.params.get_bool("RecordFrontLock") else (lambda: not ui_state.engaged))
|
||||
self._record_audio.set_enabled(lambda: not ui_state.engaged)
|
||||
ui_state.add_engaged_transition_callback(self._update_toggles)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._update_toggles()
|
||||
|
||||
def _update_toggles(self):
|
||||
ui_state.update_params()
|
||||
for key, item in self._refresh_toggles:
|
||||
item.set_checked(ui_state.params.get_bool(key))
|
||||
129
iqpilot/selfdrive/ui/mici/layouts/settings/developer.py
Normal file
129
iqpilot/selfdrive/ui/mici/layouts/settings/developer.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from iqpilot.common.time_helpers import system_time_valid
|
||||
from iqpilot.system.hardware.tici.usb_storage import apply_usb_storage_state
|
||||
from iqpilot.system.ui.widgets.scroller import NavScroller
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, BigToggle, BigCircleParamControl
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_dialog import BigDialog, BigInputDialog
|
||||
from iqpilot.system.ui.lib.application import gui_app
|
||||
from iqpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.selfdrive.ui.widgets.ssh_key import SshKeyFetcher
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
|
||||
|
||||
class DeveloperLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._ssh_fetcher = SshKeyFetcher(ui_state.params)
|
||||
|
||||
def github_username_callback(username: str):
|
||||
if username:
|
||||
self._ssh_keys_btn.set_value(tr("Loading..."))
|
||||
self._ssh_keys_btn.set_enabled(False)
|
||||
|
||||
def on_response(error):
|
||||
self._ssh_keys_btn.set_enabled(True)
|
||||
if error is None:
|
||||
self._ssh_keys_btn.set_value(username)
|
||||
else:
|
||||
self._ssh_keys_btn.set_value(tr("Not set"))
|
||||
gui_app.push_widget(BigDialog("", error))
|
||||
|
||||
self._ssh_fetcher.fetch(username, on_response)
|
||||
else:
|
||||
self._ssh_fetcher.clear()
|
||||
self._ssh_keys_btn.set_value(tr("Not set"))
|
||||
|
||||
def ssh_keys_callback():
|
||||
github_username = ui_state.params.get("GithubUsername") or ""
|
||||
dlg = BigInputDialog(tr("enter GitHub username..."), github_username, minimum_length=0, confirm_callback=github_username_callback)
|
||||
if not system_time_valid():
|
||||
dlg = BigDialog("", tr("Please connect to Wi-Fi to fetch your key."))
|
||||
gui_app.push_widget(dlg)
|
||||
return
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
txt_ssh = gui_app.texture("icons_mici/settings/developer/ssh.png", 56, 64)
|
||||
github_username = ui_state.params.get("GithubUsername") or ""
|
||||
self._ssh_keys_btn = BigButton(tr("SSH keys"), tr("Not set") if not github_username else github_username, icon=txt_ssh)
|
||||
self._ssh_keys_btn.set_click_callback(ssh_keys_callback)
|
||||
|
||||
self._adb_toggle = BigCircleParamControl(gui_app.texture("icons_mici/adb_short.png", 82, 82), "AdbEnabled", icon_offset=(0, 12))
|
||||
self._usb_storage_toggle = BigCircleParamControl(gui_app.texture("icons_mici/adb_short.png", 82, 82), "UsbStorageEnabled",
|
||||
toggle_callback=apply_usb_storage_state, icon_offset=(0, 12))
|
||||
self._ssh_toggle = BigCircleParamControl(gui_app.texture("icons_mici/ssh_short.png", 82, 82), "SshEnabled", icon_offset=(0, 12))
|
||||
self._long_maneuver_toggle = BigToggle(tr("longitudinal maneuver mode"),
|
||||
initial_state=ui_state.params.get_bool("LongitudinalManeuverMode"),
|
||||
toggle_callback=self._on_long_maneuver_mode)
|
||||
self._lat_maneuver_toggle = BigToggle(tr("lateral maneuver mode"),
|
||||
initial_state=ui_state.params.get_bool("LateralManeuverMode"),
|
||||
toggle_callback=self._on_lat_maneuver_mode)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
self._adb_toggle,
|
||||
self._usb_storage_toggle,
|
||||
self._ssh_toggle,
|
||||
self._ssh_keys_btn,
|
||||
self._long_maneuver_toggle,
|
||||
self._lat_maneuver_toggle,
|
||||
])
|
||||
|
||||
self._refresh_toggles = (
|
||||
("AdbEnabled", self._adb_toggle),
|
||||
("UsbStorageEnabled", self._usb_storage_toggle),
|
||||
("SshEnabled", self._ssh_toggle),
|
||||
("LongitudinalManeuverMode", self._long_maneuver_toggle),
|
||||
("LateralManeuverMode", self._lat_maneuver_toggle),
|
||||
)
|
||||
onroad_blocked_toggles = (self._adb_toggle, self._usb_storage_toggle)
|
||||
release_blocked_toggles = (self._long_maneuver_toggle, self._lat_maneuver_toggle)
|
||||
engaged_blocked_toggles = (self._long_maneuver_toggle, self._lat_maneuver_toggle)
|
||||
|
||||
for item in release_blocked_toggles:
|
||||
item.set_visible(not ui_state.is_release)
|
||||
|
||||
for item in onroad_blocked_toggles:
|
||||
item.set_enabled(lambda: ui_state.is_offroad())
|
||||
|
||||
for item in engaged_blocked_toggles:
|
||||
item.set_enabled(lambda: not ui_state.engaged)
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._update_toggles)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
self._ssh_fetcher.update()
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._update_toggles()
|
||||
|
||||
def _update_toggles(self):
|
||||
ui_state.update_params()
|
||||
|
||||
if ui_state.CP is not None:
|
||||
long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad()
|
||||
self._long_maneuver_toggle.set_enabled(long_man_enabled)
|
||||
self._lat_maneuver_toggle.set_enabled(ui_state.is_offroad())
|
||||
else:
|
||||
self._long_maneuver_toggle.set_enabled(False)
|
||||
self._lat_maneuver_toggle.set_enabled(False)
|
||||
|
||||
for key, item in self._refresh_toggles:
|
||||
item.set_checked(ui_state.params.get_bool(key))
|
||||
|
||||
def _on_long_maneuver_mode(self, state: bool):
|
||||
ui_state.params.put_bool("LongitudinalManeuverMode", state)
|
||||
ui_state.params.put_bool("LateralManeuverMode", False)
|
||||
self._lat_maneuver_toggle.set_checked(False)
|
||||
restart_needed_callback()
|
||||
|
||||
def _on_lat_maneuver_mode(self, state: bool):
|
||||
ui_state.params.put_bool("LateralManeuverMode", state)
|
||||
ui_state.params.put_bool("ExperimentalMode", False)
|
||||
ui_state.params.put_bool("LongitudinalManeuverMode", False)
|
||||
self._long_maneuver_toggle.set_checked(False)
|
||||
restart_needed_callback()
|
||||
250
iqpilot/selfdrive/ui/mici/layouts/settings/device.py
Normal file
250
iqpilot/selfdrive/ui/mici/layouts/settings/device.py
Normal file
@@ -0,0 +1,250 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
|
||||
from iqpilot.common.basedir import BASEDIR
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.common.time_helpers import system_time_valid
|
||||
from iqpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, BigCircleButton
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_dialog import BigDialog, BigConfirmationDialog
|
||||
from iqpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_pairing_dialog import PairingDialog
|
||||
from iqpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
|
||||
from iqpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage
|
||||
from iqpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from iqpilot.system.ui.lib.multilang import multilang, tr
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
from iqpilot.selfdrive.ui.ui_state import device, ui_state
|
||||
from iqpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from iqpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer
|
||||
from iqpilot.konn3kt.registration import UNREGISTERED_DONGLE_ID
|
||||
|
||||
|
||||
class ReviewTermsPage(TermsPage, NavScroller):
|
||||
"""TermsPage with NavWidget swipe-to-dismiss for reviewing in device settings."""
|
||||
def __init__(self):
|
||||
super().__init__(on_accept=self.dismiss, on_decline=self.dismiss)
|
||||
self._continue_button.set_visible(False)
|
||||
self._back_button.set_visible(False)
|
||||
|
||||
|
||||
class ReviewTrainingGuide(TrainingGuide):
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
device.set_override_interactive_timeout(300)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
device.set_override_interactive_timeout(None)
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||
|
||||
|
||||
class MiciFccModal(NavRawScrollPanel):
|
||||
def __init__(self, file_path: str | None = None, text: str | None = None):
|
||||
super().__init__()
|
||||
self._content = HtmlRenderer(file_path=file_path, text=text)
|
||||
self._fcc_logo = gui_app.texture("icons_mici/settings/device/fcc_logo.png", 76, 64)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
content_height = self._content.get_total_height(int(rect.width))
|
||||
content_height += self._fcc_logo.height + 20
|
||||
|
||||
scroll_content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
|
||||
scroll_offset = round(self._scroll_panel.update(rect, scroll_content_rect.height))
|
||||
|
||||
fcc_pos = rl.Vector2(rect.x + 20, rect.y + 20 + scroll_offset)
|
||||
|
||||
scroll_content_rect.y += scroll_offset + self._fcc_logo.height + 20
|
||||
self._content.render(scroll_content_rect)
|
||||
|
||||
rl.draw_texture_ex(self._fcc_logo, fcc_pos, 0.0, 1.0, rl.WHITE)
|
||||
|
||||
|
||||
def _engaged_confirmation_click(callback: Callable, action_text: str, icon: rl.Texture, exit_on_confirm: bool = True, red: bool = False):
|
||||
if not ui_state.engaged:
|
||||
def confirm_callback():
|
||||
# Check engaged again in case it changed while the dialog was open
|
||||
# TODO: if true, we stay on the dialog if not exit_on_confirm until normal onroad timeout
|
||||
if not ui_state.engaged:
|
||||
callback()
|
||||
|
||||
gui_app.push_widget(BigConfirmationDialog(tr("slide to\n{}").format(action_text.lower()), icon, confirm_callback, exit_on_confirm=exit_on_confirm, red=red))
|
||||
else:
|
||||
gui_app.push_widget(BigDialog("", tr("Disengage to {}").format(action_text)))
|
||||
|
||||
|
||||
class EngagedConfirmationCircleButton(BigCircleButton):
|
||||
def __init__(self, title: str, icon: rl.Texture, callback: Callable[[], None], exit_on_confirm: bool = True,
|
||||
red: bool = False, icon_offset: tuple[int, int] = (0, 0)):
|
||||
super().__init__(icon, red, icon_offset)
|
||||
self.set_click_callback(lambda: _engaged_confirmation_click(callback, title, icon, exit_on_confirm=exit_on_confirm, red=red))
|
||||
|
||||
|
||||
class EngagedConfirmationButton(BigButton):
|
||||
def __init__(self, text: str, action_text: str, icon: rl.Texture, callback: Callable[[], None],
|
||||
exit_on_confirm: bool = True, red: bool = False):
|
||||
super().__init__(text, "", icon)
|
||||
self.set_click_callback(lambda: _engaged_confirmation_click(callback, action_text, icon, exit_on_confirm=exit_on_confirm, red=red))
|
||||
|
||||
|
||||
class DeviceInfoLayoutMici(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.set_rect(rl.Rectangle(0, 0, 360, 180))
|
||||
|
||||
params = Params()
|
||||
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
|
||||
max_width = int(self._rect.width - 20)
|
||||
self._dongle_id_label = UnifiedLabel(tr("device ID"), 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False)
|
||||
self._dongle_id_text_label = UnifiedLabel(params.get("DongleId") or 'N/A', 32, max_width=max_width, text_color=subheader_color,
|
||||
font_weight=FontWeight.ROMAN, wrap_text=False)
|
||||
|
||||
self._serial_number_label = UnifiedLabel(tr("serial"), 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False)
|
||||
self._serial_number_text_label = UnifiedLabel(params.get("HardwareSerial") or 'N/A', 32, max_width=max_width, text_color=subheader_color,
|
||||
font_weight=FontWeight.ROMAN, wrap_text=False)
|
||||
|
||||
def _render(self, _):
|
||||
self._dongle_id_label.set_position(self._rect.x + 20, self._rect.y - 10)
|
||||
self._dongle_id_label.render()
|
||||
|
||||
self._dongle_id_text_label.set_position(self._rect.x + 20, self._rect.y + 68 - 25)
|
||||
self._dongle_id_text_label.render()
|
||||
|
||||
self._serial_number_label.set_position(self._rect.x + 20, self._rect.y + 114 - 30)
|
||||
self._serial_number_label.render()
|
||||
|
||||
self._serial_number_text_label.set_position(self._rect.x + 20, self._rect.y + 161 - 25)
|
||||
self._serial_number_text_label.render()
|
||||
|
||||
|
||||
class PairBigButton(BigButton):
|
||||
"""Konn3kt connect button: logo + live connection-status dot. Uses the new accent box style."""
|
||||
KONN3KT_ONLINE_NS = 80_000_000_000 # 80 seconds in nanoseconds
|
||||
STATUS_ONLINE = rl.Color(0x86, 0xFF, 0x4E, 255)
|
||||
STATUS_OFFLINE = rl.Color(0xC9, 0x22, 0x31, 255)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("konn3kt", tr("pair in app"), gui_app.texture("icons_mici/settings/konn3kt_icon.png", 56, 56))
|
||||
|
||||
def _get_label_font_size(self):
|
||||
return 64
|
||||
|
||||
def _is_konn3kt_online(self) -> bool:
|
||||
last_ping = ui_state.sm['deviceState'].lastAthenaPingTime
|
||||
return last_ping != 0 and (time.monotonic_ns() - last_ping) < self.KONN3KT_ONLINE_NS
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if ui_state.prime_state.is_paired():
|
||||
self.set_value(tr("online") if self._is_konn3kt_online() else tr("offline"))
|
||||
else:
|
||||
self.set_value(tr("pair in app"))
|
||||
|
||||
def _render(self, _):
|
||||
super()._render(_)
|
||||
if ui_state.prime_state.is_paired():
|
||||
color = self.STATUS_ONLINE if self._is_konn3kt_online() else self.STATUS_OFFLINE
|
||||
rl.draw_circle(int(self._rect.x + 30), int(self._rect.y + 30), 9, color)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
if ui_state.prime_state.is_paired():
|
||||
return
|
||||
dlg: BigDialog | PairingDialog
|
||||
if not system_time_valid():
|
||||
dlg = BigDialog("", tr("Please connect to Wi-Fi to complete initial pairing."))
|
||||
elif UNREGISTERED_DONGLE_ID == (ui_state.params.get("DongleId") or UNREGISTERED_DONGLE_ID):
|
||||
dlg = BigDialog("", tr("Device must be registered with Konn3kt to pair."))
|
||||
else:
|
||||
dlg = PairingDialog()
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
|
||||
class DeviceLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._fcc_dialog: HtmlModal | None = None
|
||||
|
||||
def power_off_callback():
|
||||
ui_state.params.put_bool("DoShutdown", True)
|
||||
|
||||
def reboot_callback():
|
||||
ui_state.params.put_bool("DoReboot", True)
|
||||
|
||||
def reset_calibration_callback():
|
||||
params = ui_state.params
|
||||
params.remove("CalibrationParams")
|
||||
params.remove("LiveTorqueParameters")
|
||||
params.remove("LiveParameters")
|
||||
params.remove("LiveParametersV2")
|
||||
params.remove("LiveDelay")
|
||||
params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
reset_calibration_btn = EngagedConfirmationButton(tr("reset calibration"), tr("reset"), gui_app.texture("icons_mici/settings/device/lkas.png", 122, 64),
|
||||
reset_calibration_callback)
|
||||
|
||||
reboot_btn = EngagedConfirmationCircleButton(tr("reboot"), gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70),
|
||||
reboot_callback, exit_on_confirm=False)
|
||||
|
||||
self._power_off_btn = EngagedConfirmationCircleButton(tr("power off"), gui_app.texture("icons_mici/settings/device/power.png", 64, 66),
|
||||
power_off_callback, exit_on_confirm=False, red=True)
|
||||
self._power_off_btn.set_visible(lambda: not ui_state.ignition)
|
||||
|
||||
regulatory_btn = BigButton(tr("regulatory info"), "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64))
|
||||
regulatory_btn.set_click_callback(self._on_regulatory)
|
||||
|
||||
driver_cam_btn = BigButton(tr("driver\ncamera preview"), "", gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64))
|
||||
driver_cam_btn.set_click_callback(lambda: gui_app.push_widget(DriverCameraDialog()))
|
||||
driver_cam_btn.set_enabled(lambda: ui_state.is_offroad())
|
||||
|
||||
review_training_guide_btn = BigButton(tr("review\ntraining guide"), "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64))
|
||||
review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTrainingGuide(completed_callback=lambda: gui_app.pop_widgets_to(self))))
|
||||
review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad())
|
||||
|
||||
terms_btn = BigButton(tr("terms &\nconditions"), "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64))
|
||||
terms_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTermsPage()))
|
||||
|
||||
language_btn = BigButton(tr("change\nlanguage"), multilang.codes.get(multilang.language, ""),
|
||||
gui_app.texture("icons_mici/settings/device/language.png", 64, 64))
|
||||
language_btn.set_click_callback(self._on_change_language)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
DeviceInfoLayoutMici(),
|
||||
PairBigButton(),
|
||||
review_training_guide_btn,
|
||||
driver_cam_btn,
|
||||
language_btn,
|
||||
terms_btn,
|
||||
regulatory_btn,
|
||||
reset_calibration_btn,
|
||||
reboot_btn,
|
||||
self._power_off_btn,
|
||||
])
|
||||
|
||||
def _on_change_language(self):
|
||||
names = list(multilang.languages.keys())
|
||||
if not names:
|
||||
return
|
||||
current = multilang.codes.get(multilang.language)
|
||||
dlg = BigMultiOptionDialog(names, current if current in names else names[0], right_btn="check",
|
||||
right_btn_callback=lambda: self._apply_language(dlg.get_selected_option()))
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
def _apply_language(self, name: str):
|
||||
code = multilang.languages.get(name)
|
||||
if code and code != multilang.language:
|
||||
multilang.change_language(code)
|
||||
|
||||
def _on_regulatory(self):
|
||||
if not self._fcc_dialog:
|
||||
self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "iqpilot/selfdrive/assets/offroad/mici_fcc.html"))
|
||||
gui_app.push_widget(self._fcc_dialog)
|
||||
56
iqpilot/selfdrive/ui/mici/layouts/settings/display.py
Normal file
56
iqpilot/selfdrive/ui/mici/layouts/settings/display.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigParamControl
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.iq_widgets import MappedParamToggle
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.system.hardware import HARDWARE
|
||||
from iqpilot.system.ui.widgets.scroller import NavScroller
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
|
||||
_BRIGHT_PERCENTS = [f"{p}%" for p in range(5, 101, 5)]
|
||||
_DISPLAY_BRIGHT_VALUES = [0] + list(range(5, 101, 5))
|
||||
|
||||
_ONROAD_BRIGHT_OPTIONS = ["auto", "auto dark"] + _BRIGHT_PERCENTS
|
||||
_ONROAD_BRIGHT_VALUES = list(range(len(_ONROAD_BRIGHT_OPTIONS)))
|
||||
|
||||
_DELAY_OPTIONS = ["15s", "30s", "1m", "2m", "3m", "4m", "5m", "6m", "7m", "8m", "9m", "10m"]
|
||||
_DELAY_VALUES = [15, 30, 60, 120, 180, 240, 300, 360, 420, 480, 540, 600]
|
||||
|
||||
_INTERACT_SUFFIX = ["10s", "20s", "30s", "40s", "50s", "1m", "70s", "80s", "90s", "100s", "110s", "2m"]
|
||||
_INTERACT_VALUES = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]
|
||||
|
||||
|
||||
class DisplayLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._force_mici = BigParamControl(tr("force mici UI"), "ForceSmallUI")
|
||||
self._display_bright = MappedParamToggle(tr("display brightness"), "Brightness",
|
||||
[tr("default")] + _BRIGHT_PERCENTS, _DISPLAY_BRIGHT_VALUES)
|
||||
self._onroad_bright = MappedParamToggle(tr("driving brightness"), "OnroadScreenOffBrightness",
|
||||
[tr("auto"), tr("auto dark")] + _BRIGHT_PERCENTS, _ONROAD_BRIGHT_VALUES)
|
||||
self._delay = MappedParamToggle(tr("brightness delay"), "OnroadScreenOffTimer",
|
||||
_DELAY_OPTIONS, _DELAY_VALUES)
|
||||
self._interact = MappedParamToggle(tr("interactivity"), "InteractivityTimeout",
|
||||
[tr("default")] + _INTERACT_SUFFIX, _INTERACT_VALUES)
|
||||
|
||||
self._items = [self._display_bright, self._onroad_bright, self._delay, self._interact]
|
||||
if HARDWARE.get_device_type() != "mici":
|
||||
self._items.insert(0, self._force_mici)
|
||||
self._scroller.add_widgets(self._items)
|
||||
|
||||
def _refresh(self):
|
||||
for w in self._items:
|
||||
w.refresh()
|
||||
bval = int(float(ui_state.params.get("OnroadScreenOffBrightness", return_default=True) or 0))
|
||||
self._delay.set_enabled(bval not in (0, 1))
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
bval = int(float(ui_state.params.get("OnroadScreenOffBrightness", return_default=True) or 0))
|
||||
self._delay.set_enabled(bval not in (0, 1))
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._refresh()
|
||||
89
iqpilot/selfdrive/ui/mici/layouts/settings/drive_history.py
Normal file
89
iqpilot/selfdrive/ui/mici/layouts/settings/drive_history.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from iqpilot.common.api import api_get
|
||||
from iqpilot.common.constants import CV
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.common.swaglog import cloudlog
|
||||
from iqpilot.selfdrive.ui.lib.api_helpers import get_token
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from iqpilot.konn3kt.registration import UNREGISTERED_DONGLE_ID
|
||||
from iqpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from iqpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from iqpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
|
||||
_ACCENT = rl.Color(0x3A, 0xDD, 0xC6, 255)
|
||||
_CARD_BG = rl.Color(26, 27, 30, 255)
|
||||
_LABEL = rl.Color(150, 150, 150, 255)
|
||||
|
||||
|
||||
class TripsLayoutMici(NavWidget):
|
||||
PARAM_KEY = "ApiCache_DriveStats"
|
||||
UPDATE_INTERVAL = 30
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._stats = self._params.get(self.PARAM_KEY) or {}
|
||||
self._bold = gui_app.font(FontWeight.BOLD)
|
||||
self._medium = gui_app.font(FontWeight.MEDIUM)
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._update_loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def __del__(self):
|
||||
self._running = False
|
||||
|
||||
def _fetch(self):
|
||||
try:
|
||||
dongle_id = self._params.get("DongleId")
|
||||
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
|
||||
return
|
||||
resp = api_get(f"v1.1/devices/{dongle_id}/stats", access_token=get_token(dongle_id))
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
self._stats = data
|
||||
self._params.put(self.PARAM_KEY, data)
|
||||
except Exception as e:
|
||||
cloudlog.error(f"trips: failed to fetch drive stats: {e}")
|
||||
|
||||
def _update_loop(self):
|
||||
while self._running:
|
||||
if not ui_state.started and device._awake:
|
||||
self._fetch()
|
||||
time.sleep(self.UPDATE_INTERVAL)
|
||||
|
||||
def _render_group(self, x, y, w, h, title, data, is_metric):
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(x, y, w, h), 0.16, 8, _CARD_BG)
|
||||
rl.draw_text_ex(self._bold, title, rl.Vector2(x + 22, y + 12), 24, 0, _ACCENT)
|
||||
|
||||
routes = int(data.get("routes", 0) or 0)
|
||||
distance = data.get("distance", 0) or 0
|
||||
dist = int(distance * CV.MPH_TO_KPH) if is_metric else int(distance)
|
||||
hours = int((data.get("minutes", 0) or 0) / 60)
|
||||
cols = [(str(routes), "drives"), (str(dist), "km" if is_metric else "mi"), (str(hours), "hours")]
|
||||
|
||||
col_w = w / 3
|
||||
for i, (val, lbl) in enumerate(cols):
|
||||
cx = x + col_w * i + col_w / 2
|
||||
vs = measure_text_cached(self._bold, val, 46)
|
||||
rl.draw_text_ex(self._bold, val, rl.Vector2(cx - vs.x / 2, y + h / 2 - 28), 46, 0, rl.WHITE)
|
||||
ls = measure_text_cached(self._medium, lbl, 22)
|
||||
rl.draw_text_ex(self._medium, lbl, rl.Vector2(cx - ls.x / 2, y + h / 2 + 24), 22, 0, _LABEL)
|
||||
|
||||
def _render(self, _):
|
||||
rect = self._rect
|
||||
is_metric = self._params.get_bool("IsMetric")
|
||||
stats = self._stats if isinstance(self._stats, dict) else {}
|
||||
pad = 12
|
||||
h = (rect.height - 3 * pad) / 2
|
||||
w = rect.width - 2 * pad
|
||||
x = rect.x + pad
|
||||
self._render_group(x, rect.y + pad, w, h, "ALL TIME", stats.get("all", {}), is_metric)
|
||||
self._render_group(x, rect.y + 2 * pad + h, w, h, "PAST WEEK", stats.get("week", {}), is_metric)
|
||||
145
iqpilot/selfdrive/ui/mici/layouts/settings/iq_widgets.py
Normal file
145
iqpilot/selfdrive/ui/mici/layouts/settings/iq_widgets.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from iqpilot.common.params import Params, UnknownKeyName
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, BigMultiToggle, BigToggle, BigParamControl
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
|
||||
|
||||
class SafeParamControl(BigParamControl):
|
||||
"""BigParamControl that tolerates a param missing from the COMPILED params registry.
|
||||
|
||||
A key added to params_keys.h only exists at runtime once params_pyx.so is rebuilt; a
|
||||
.py-only / stale prebuilt deploy leaves get_bool/put_bool raising UnknownKeyName, which
|
||||
would crash the UI on construction. Default to `default_on` for display and no-op the
|
||||
write instead of crashing — mirrors the plannerd defensive read in long_mpc.py.
|
||||
"""
|
||||
|
||||
def __init__(self, text: str, param: str, default_on: bool = True, toggle_callback=None):
|
||||
self._default_on = default_on
|
||||
BigToggle.__init__(self, text, "", toggle_callback=toggle_callback)
|
||||
self.param = param
|
||||
self.params = Params()
|
||||
self.set_checked(self._safe_get())
|
||||
|
||||
def _safe_get(self) -> bool:
|
||||
try:
|
||||
return self.params.get_bool(self.param)
|
||||
except UnknownKeyName:
|
||||
return self._default_on
|
||||
|
||||
def refresh(self):
|
||||
self.set_checked(self._safe_get())
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos):
|
||||
BigToggle._handle_mouse_release(self, mouse_pos)
|
||||
try:
|
||||
self.params.put_bool(self.param, self._checked)
|
||||
except UnknownKeyName:
|
||||
pass
|
||||
|
||||
|
||||
class MappedParamToggle(BigMultiToggle):
|
||||
"""Multi-option toggle whose options map to arbitrary param values (int or float, drum-style).
|
||||
|
||||
Up to PILL_LIMIT options render as the stock vertical pill column; more options would
|
||||
overflow the box, so they instead show the current value as a sub-label and cycle on tap.
|
||||
"""
|
||||
PILL_LIMIT = 4
|
||||
|
||||
def __init__(self, text: str, param: str, options: list[str], values: list | None = None):
|
||||
super().__init__(text, options)
|
||||
self._param = param
|
||||
self._values = values if values is not None else list(range(len(options)))
|
||||
self._params = Params()
|
||||
self.refresh()
|
||||
|
||||
def _value_only(self) -> bool:
|
||||
return len(self._options) > self.PILL_LIMIT
|
||||
|
||||
def _width_hint(self) -> int:
|
||||
if self._value_only():
|
||||
return BigButton._width_hint(self)
|
||||
return super()._width_hint()
|
||||
|
||||
def _draw_content(self, btn_x: float, btn_y: float, btn_width: float, btn_height: float):
|
||||
if self._value_only():
|
||||
BigButton._draw_content(self, btn_x, btn_y, btn_width, btn_height)
|
||||
else:
|
||||
super()._draw_content(btn_x, btn_y, btn_width, btn_height)
|
||||
|
||||
def refresh(self):
|
||||
try:
|
||||
raw = self._params.get(self._param, return_default=True)
|
||||
except UnknownKeyName:
|
||||
raw = self._values[0]
|
||||
try:
|
||||
cur = float(raw)
|
||||
except (TypeError, ValueError):
|
||||
cur = float(self._values[0])
|
||||
idx = min(range(len(self._values)), key=lambda i: abs(float(self._values[i]) - cur))
|
||||
self.set_value(self._options[idx])
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
idx = self._options.index(self.value)
|
||||
try:
|
||||
self._params.put(self._param, self._values[idx])
|
||||
except UnknownKeyName:
|
||||
pass
|
||||
|
||||
|
||||
class IQModeSelector(BigMultiToggle):
|
||||
"""Longitudinal mode selector: Stock ACC / IQ.Chill / IQ.Dynamic / IQ.Pilot.
|
||||
|
||||
A single tap cycles to the next mode and applies the matching param combo immediately.
|
||||
"""
|
||||
OPTIONS = ["Stock ACC", "IQ.Chill", "IQ.Dynamic", "IQ.Pilot"]
|
||||
PERSONALITY_RELAXED = 2
|
||||
|
||||
def __init__(self):
|
||||
self._display_options = [tr(option) for option in self.OPTIONS]
|
||||
super().__init__(tr("IQ Mode"), self._display_options)
|
||||
self._params = Params()
|
||||
self.refresh()
|
||||
|
||||
def _index(self) -> int:
|
||||
p = self._params
|
||||
if not p.get_bool("AlphaLongitudinalEnabled"):
|
||||
return 0
|
||||
if not p.get_bool("ExperimentalMode"):
|
||||
return 1
|
||||
return 2 if p.get_bool("IQDynamicMode") else 3
|
||||
|
||||
def is_dynamic(self) -> bool:
|
||||
return self._index() == 2
|
||||
|
||||
def refresh(self):
|
||||
self.set_value(self._display_options[self._index()])
|
||||
|
||||
def _apply(self, idx: int):
|
||||
p = self._params
|
||||
if idx == 0:
|
||||
p.put_bool("AlphaLongitudinalEnabled", False)
|
||||
p.put_bool("ExperimentalMode", False)
|
||||
p.put_bool("IQDynamicMode", False)
|
||||
elif idx == 1:
|
||||
p.put_bool("AlphaLongitudinalEnabled", True)
|
||||
p.put_bool("ExperimentalMode", False)
|
||||
p.put_bool("IQDynamicMode", False)
|
||||
p.put("LongitudinalPersonality", self.PERSONALITY_RELAXED)
|
||||
elif idx == 2:
|
||||
p.put_bool("AlphaLongitudinalEnabled", True)
|
||||
p.put_bool("ExperimentalMode", True)
|
||||
p.put_bool("IQDynamicMode", True)
|
||||
else:
|
||||
p.put_bool("AlphaLongitudinalEnabled", True)
|
||||
p.put_bool("ExperimentalMode", True)
|
||||
p.put_bool("IQDynamicMode", False)
|
||||
p.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos):
|
||||
nxt = (self._index() + 1) % len(self.OPTIONS)
|
||||
self._apply(nxt)
|
||||
self.set_value(self._display_options[nxt])
|
||||
499
iqpilot/selfdrive/ui/mici/layouts/settings/models.py
Normal file
499
iqpilot/selfdrive/ui/mici/layouts/settings/models.py
Normal file
@@ -0,0 +1,499 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from iqpilot.cereal import custom
|
||||
|
||||
from iqpilot.system.ui.iqwidgets.widgets.helpers.glyphs import draw_star
|
||||
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.system.ui.lib.application import gui_app
|
||||
from iqpilot.system.ui.widgets.scroller import NavScroller
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, BigParamControl, GreyBigButton
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_dialog import BigConfirmationDialog
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.iq_widgets import MappedParamToggle
|
||||
|
||||
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.system.ui.lib.multilang import tr
|
||||
|
||||
_DELAY_OPTIONS = ["0.05s", "0.10s", "0.15s", "0.20s", "0.25s", "0.30s", "0.35s", "0.40s", "0.45s", "0.50s"]
|
||||
_DELAY_VALUES = [0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40, 0.45, 0.50]
|
||||
|
||||
_LANE_TURN_VALUES = [15.0, 19.0, 20.0]
|
||||
|
||||
_DL = custom.IQModelManager.DownloadStatus
|
||||
_ACTIVE_BUNDLE_KEY = "ModelManager_ActiveBundle"
|
||||
_DOWNLOAD_INDEX_KEY = "ModelManager_DownloadIndex"
|
||||
_RUNNER_CACHE_KEY = "ModelRunnerTypeCache"
|
||||
|
||||
|
||||
def _display_model_name(bundle) -> str:
|
||||
return bundle.internalName if getattr(bundle, "internalName", "") else bundle.displayName
|
||||
|
||||
|
||||
class _ModelSelectPanel(NavScroller):
|
||||
"""A throwaway scroller panel (folder list or bundle list) pushed onto the nav stack."""
|
||||
def __init__(self, items):
|
||||
super().__init__()
|
||||
self._scroller.add_widgets(items)
|
||||
|
||||
|
||||
class _ModelButton(BigButton):
|
||||
"""A bundle in the model list: single tap selects (download), double tap toggles favorite.
|
||||
|
||||
A golden star is drawn in the corner when the model is favorited.
|
||||
"""
|
||||
THRESHOLD = 0.4
|
||||
_STAR_GOLD = rl.Color(0xFF, 0xC1, 0x07, 255)
|
||||
|
||||
def __init__(self, bundle, on_select, on_favorite, is_favorite):
|
||||
super().__init__(bundle.displayName)
|
||||
self._bundle = bundle
|
||||
self._on_select = on_select
|
||||
self._on_favorite = on_favorite
|
||||
self._is_favorite = is_favorite
|
||||
self._pending_t = 0.0
|
||||
self._pending_pos = None
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos):
|
||||
now = time.monotonic()
|
||||
if self._pending_pos is not None and now - self._pending_t < self.THRESHOLD:
|
||||
self._pending_pos = None
|
||||
self._pending_t = 0.0
|
||||
self._is_favorite = self._on_favorite(self._bundle)
|
||||
return
|
||||
self._pending_t = now
|
||||
self._pending_pos = mouse_pos
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if self._pending_pos is not None and time.monotonic() - self._pending_t >= self.THRESHOLD:
|
||||
self._pending_pos = None
|
||||
self._on_select(self._bundle)
|
||||
|
||||
def _render(self, _):
|
||||
super()._render(_)
|
||||
if self._is_favorite:
|
||||
cx = self._rect.x + self._rect.width - 46
|
||||
cy = self._rect.y + 46
|
||||
draw_star(cx, cy, 24, True, self._STAR_GOLD)
|
||||
|
||||
|
||||
class ModelsLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._last_cache_t = 0.0
|
||||
self._download_status = None
|
||||
self._prev_download_status = None
|
||||
self._clear_icon = gui_app.texture("icons_mici/settings/developer_icon.png", 56, 56)
|
||||
self._redownload_icon = gui_app.texture("icons_mici/settings/device/update.png", 56, 56, keep_aspect_ratio=True)
|
||||
self._reset_icon = gui_app.texture("icons_mici/wheel.png", 56, 56)
|
||||
|
||||
self._current = BigButton(tr("active model"))
|
||||
self._current.set_click_callback(self._show_folders)
|
||||
|
||||
self._cancel = BigButton(tr("stop download"))
|
||||
self._cancel.set_click_callback(self._cancel_model_request)
|
||||
self._cancel.set_visible(self._is_downloading)
|
||||
|
||||
self._redownload = BigButton(tr("redownload model"))
|
||||
self._redownload.set_click_callback(self._confirm_redownload_model)
|
||||
self._redownload.set_enabled(self._can_redownload)
|
||||
|
||||
self._refresh = BigButton(tr("reload model list"))
|
||||
self._refresh.set_click_callback(lambda: ui_state.params.put("ModelManager_LastSyncTime", 0))
|
||||
|
||||
self._supercombo = GreyBigButton(tr("combined model"))
|
||||
self._supercombo.set_visible(False)
|
||||
self._vision = GreyBigButton(tr("vision weights"))
|
||||
self._vision.set_visible(False)
|
||||
self._policy = GreyBigButton(tr("policy weights"))
|
||||
self._policy.set_visible(False)
|
||||
|
||||
self._clear = BigButton(tr("purge model cache"))
|
||||
self._clear.set_click_callback(self._confirm_clear_cache)
|
||||
self._clear.set_enabled(lambda: ui_state.is_offroad())
|
||||
|
||||
self._steer_delay = BigParamControl(tr("self-tuning steer delay"), "IQLiveSteerDelay")
|
||||
self._sw_delay = MappedParamToggle(tr("manual delay offset"), "IQSoftwareSteerDelay", _DELAY_OPTIONS, _DELAY_VALUES)
|
||||
self._sw_delay.set_visible(lambda: not self._steer_delay._checked)
|
||||
|
||||
self._lane_turn = BigParamControl(tr("low-speed turn planning"), "IQLaneTurnDesire")
|
||||
self._lane_speed = MappedParamToggle(tr("lane turn speed"), "IQLaneTurnValue", [tr("slow"), tr("normal"), tr("fast")], _LANE_TURN_VALUES)
|
||||
self._lane_speed.set_visible(lambda: self._lane_turn._checked)
|
||||
|
||||
self._main_items = [self._current, self._cancel, self._supercombo, self._vision, self._policy, self._redownload, self._refresh, self._clear,
|
||||
self._steer_delay, self._sw_delay, self._lane_turn, self._lane_speed]
|
||||
self._scroller.add_widgets(self._main_items)
|
||||
|
||||
@property
|
||||
def model_manager(self):
|
||||
return ui_state.sm["iqModelManager"]
|
||||
|
||||
@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()
|
||||
|
||||
def _is_downloading(self) -> bool:
|
||||
try:
|
||||
return bool(self.model_manager.selectedBundle and self.model_manager.selectedBundle.status == _DL.downloading)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _calculate_cache_size() -> float:
|
||||
if os.path.exists(CUSTOM_MODEL_PATH):
|
||||
return sum(os.path.getsize(os.path.join(CUSTOM_MODEL_PATH, f)) for f in os.listdir(CUSTOM_MODEL_PATH)) / (1024 ** 2)
|
||||
return 0.0
|
||||
|
||||
@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 _group_folders(self, bundles):
|
||||
folders: dict = {}
|
||||
for bundle in bundles:
|
||||
folder = next((ov.value for ov in bundle.overrides if ov.key == "folder"), "")
|
||||
folders.setdefault(folder, []).append(bundle)
|
||||
return folders
|
||||
|
||||
@staticmethod
|
||||
def _read_favorites() -> set:
|
||||
favs = ui_state.params.get("IQModelFavorites")
|
||||
return set(favs.split(';')) if favs else set()
|
||||
|
||||
def _toggle_favorite(self, bundle) -> bool:
|
||||
favs = self._read_favorites()
|
||||
if bundle.ref in favs:
|
||||
favs.discard(bundle.ref)
|
||||
else:
|
||||
favs.add(bundle.ref)
|
||||
ui_state.params.put("IQModelFavorites", ';'.join(sorted(favs)))
|
||||
return bundle.ref in favs
|
||||
|
||||
def _confirm_clear_cache(self):
|
||||
gui_app.push_widget(BigConfirmationDialog(tr("slide to\nclear cache"), self._clear_icon,
|
||||
lambda: ui_state.params.put_bool("ModelManager_ClearCache", True),
|
||||
red=True))
|
||||
|
||||
def _redownload_target_bundle(self):
|
||||
try:
|
||||
selected = self.model_manager.selectedBundle
|
||||
if selected and selected.status == _DL.failed:
|
||||
return selected
|
||||
active = self.model_manager.activeBundle
|
||||
if self._has_active_bundle_param() and active and active.ref:
|
||||
return active
|
||||
except Exception:
|
||||
pass
|
||||
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
|
||||
|
||||
try:
|
||||
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)
|
||||
except Exception:
|
||||
pass
|
||||
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 _confirm_redownload_model(self):
|
||||
index = self._redownload_target_index()
|
||||
if index is None:
|
||||
return
|
||||
|
||||
def _redownload():
|
||||
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)
|
||||
self._redownload.set_value(tr("queued"))
|
||||
|
||||
gui_app.push_widget(BigConfirmationDialog(tr("slide to\nredownload"), self._redownload_icon, _redownload, red=True))
|
||||
|
||||
def _show_folders(self):
|
||||
bundles = list(self.model_manager.availableBundles)
|
||||
favorites = self._read_favorites()
|
||||
btns = []
|
||||
|
||||
default_btn = BigButton(tr("Default (CD210)"))
|
||||
default_btn.set_click_callback(self._select_default)
|
||||
btns.append(default_btn)
|
||||
|
||||
if favorites and (fav_bundles := [b for b in bundles if b.ref in favorites]):
|
||||
fav_btn = BigButton(tr("Favorites"), str(len(fav_bundles)))
|
||||
fav_btn.set_click_callback(lambda fb=fav_bundles: self._show_bundles(fb))
|
||||
btns.append(fav_btn)
|
||||
|
||||
folders = self._group_folders(bundles)
|
||||
for folder in sorted(folders, key=lambda f: max((b.index for b in folders[f]), default=-1), reverse=True):
|
||||
name = folder if folder else "Other"
|
||||
folder_bundles = sorted(folders[folder], key=lambda b: b.index, reverse=True)
|
||||
if folder_bundles and (m := re.search(r'\(([^)]*)\)[^(]*$', folder_bundles[0].displayName)):
|
||||
name += f" ({m.group(1)})"
|
||||
btn = BigButton(name)
|
||||
btn.set_click_callback(lambda fb=folder_bundles: self._show_bundles(fb))
|
||||
btns.append(btn)
|
||||
|
||||
gui_app.push_widget(_ModelSelectPanel(btns))
|
||||
|
||||
def _show_bundles(self, bundles):
|
||||
favorites = self._read_favorites()
|
||||
btns = [_ModelButton(b, self._select_model, self._toggle_favorite, b.ref in favorites) for b in bundles]
|
||||
gui_app.push_widget(_ModelSelectPanel(btns))
|
||||
|
||||
def _generation_changed(self, bundle) -> bool:
|
||||
try:
|
||||
active = self.model_manager.activeBundle
|
||||
return bool(active and active.ref and bundle.generation != active.generation)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _select_model(self, bundle):
|
||||
ui_state.params.put(_DOWNLOAD_INDEX_KEY, bundle.index)
|
||||
cb = self._show_reset_calibration_prompt if self._generation_changed(bundle) else lambda: None
|
||||
gui_app.pop_widgets_to(self, callback=cb)
|
||||
|
||||
def _select_default(self):
|
||||
try:
|
||||
active = self.model_manager.activeBundle
|
||||
had_custom_model = bool(active and active.ref and not is_default_bundle(active))
|
||||
except Exception:
|
||||
had_custom_model = False
|
||||
|
||||
select_default_model(ui_state.params)
|
||||
gui_app.pop_widgets_to(self, callback=self._show_reset_calibration_prompt if had_custom_model else (lambda: None))
|
||||
|
||||
def _show_reset_calibration_prompt(self):
|
||||
def _reset():
|
||||
ui_state.params.remove("CalibrationParams")
|
||||
ui_state.params.remove("LiveTorqueParameters")
|
||||
gui_app.push_widget(BigConfirmationDialog(tr("slide to\nreset calibration"), self._reset_icon, _reset))
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
self._handle_bundle_download_progress()
|
||||
self._current.set_value(self._current_model_value())
|
||||
self._current.set_enabled(ui_state.is_offroad())
|
||||
target = self._redownload_target_bundle()
|
||||
self._redownload.set_value(_display_model_name(target) if target else "")
|
||||
|
||||
now = time.monotonic()
|
||||
if now - self._last_cache_t > 1.0:
|
||||
self._last_cache_t = now
|
||||
self._clear.set_value(f"{self._calculate_cache_size():.1f} MB")
|
||||
|
||||
self._update_steer_delay_subtext()
|
||||
|
||||
def _progress_target_bundle(self):
|
||||
try:
|
||||
selected = self.model_manager.selectedBundle
|
||||
active = self.model_manager.activeBundle
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if selected and (selected.status == _DL.downloading or selected.status == _DL.failed):
|
||||
return selected
|
||||
return active if self._has_active_bundle_param() else None
|
||||
|
||||
def _handle_bundle_download_progress(self):
|
||||
labels = {
|
||||
custom.IQModelManager.Model.Type.supercombo: self._supercombo,
|
||||
custom.IQModelManager.Model.Type.vision: self._vision,
|
||||
custom.IQModelManager.Model.Type.policy: self._policy,
|
||||
}
|
||||
for label in labels.values():
|
||||
label.set_visible(False)
|
||||
label.set_value("")
|
||||
|
||||
self._cancel.set_visible(False)
|
||||
|
||||
bundle = self._progress_target_bundle()
|
||||
if not bundle:
|
||||
self._download_status = None
|
||||
self._prev_download_status = None
|
||||
return
|
||||
|
||||
self._download_status = bundle.status
|
||||
status_changed = self._download_status != self._prev_download_status
|
||||
self._prev_download_status = self._download_status
|
||||
|
||||
self._cancel.set_visible(bool(getattr(self.model_manager, "selectedBundle", None)) and self._has_download_request())
|
||||
|
||||
if self._download_status not in (_DL.downloading, _DL.failed):
|
||||
return
|
||||
|
||||
if self._download_status == _DL.downloading:
|
||||
try:
|
||||
from iqpilot.selfdrive.ui.ui_state import device
|
||||
device._reset_interactive_timeout()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for model in bundle.models:
|
||||
label = labels.get(getattr(model.type, "raw", model.type))
|
||||
if label is None:
|
||||
continue
|
||||
label.set_visible(True)
|
||||
label.set_value(self._download_label_text(bundle, model, status_changed))
|
||||
|
||||
def _download_label_text(self, bundle, model, status_changed: bool) -> str:
|
||||
p = model.artifact.downloadProgress
|
||||
if p.status == _DL.downloading:
|
||||
return f"{int(p.progress)}% downloading {_display_model_name(bundle)}"
|
||||
if p.status in (_DL.downloaded, _DL.cached):
|
||||
if self._download_status == _DL.downloading:
|
||||
return f"{_display_model_name(bundle)} ready"
|
||||
return f"{_display_model_name(bundle)} {'downloaded' if status_changed else 'ready'}"
|
||||
if p.status == _DL.failed:
|
||||
return f"download failed {_display_model_name(bundle)}"
|
||||
return f"pending {_display_model_name(bundle)}"
|
||||
|
||||
def _current_model_value(self) -> str:
|
||||
bundle = self._progress_target_bundle()
|
||||
if not bundle:
|
||||
return self._active_model_name()
|
||||
|
||||
if self._download_status == _DL.downloading:
|
||||
return self._download_progress_text(bundle)
|
||||
if self._download_status == _DL.failed:
|
||||
return f"failed: {_display_model_name(bundle)}"
|
||||
return self._active_model_name()
|
||||
|
||||
def _update_steer_delay_subtext(self):
|
||||
if self._steer_delay._checked:
|
||||
try:
|
||||
self._steer_delay.set_value(f"measured {ui_state.sm['lateralDelay'].lateralDelay:.3f} s")
|
||||
except Exception:
|
||||
self._steer_delay.set_value("")
|
||||
return
|
||||
try:
|
||||
sw = float(ui_state.params.get("IQSoftwareSteerDelay", return_default=True))
|
||||
except (TypeError, ValueError):
|
||||
sw = 0.2
|
||||
if ui_state.CP is not None:
|
||||
self._steer_delay.set_value(f"total {ui_state.CP.steerActuatorDelay + sw:.2f} s")
|
||||
else:
|
||||
self._steer_delay.set_value(f"+{sw:.2f} s offset")
|
||||
|
||||
def _active_model_name(self) -> str:
|
||||
if not self._has_active_bundle_param():
|
||||
return "Default (CD210)"
|
||||
|
||||
try:
|
||||
active = self.model_manager.activeBundle
|
||||
if is_default_bundle(active):
|
||||
return active.displayName or "Default (CD210)"
|
||||
if active and active.ref:
|
||||
return _display_model_name(active)
|
||||
except Exception:
|
||||
pass
|
||||
return "Default (CD210)"
|
||||
|
||||
def _download_progress_text(self, bundle=None) -> str:
|
||||
bundle = bundle or getattr(self.model_manager, "selectedBundle", None)
|
||||
if not bundle:
|
||||
return "downloading..."
|
||||
try:
|
||||
parts = []
|
||||
for model in bundle.models:
|
||||
p = model.artifact.downloadProgress
|
||||
if p.status == _DL.downloading:
|
||||
parts.append(f"{int(p.progress)}%")
|
||||
elif p.status in (_DL.downloaded, _DL.cached):
|
||||
parts.append("ready")
|
||||
elif p.status == _DL.failed:
|
||||
parts.append("failed")
|
||||
return f"{_display_model_name(bundle)} {' '.join(parts)}".strip() or "downloading..."
|
||||
except Exception:
|
||||
return "downloading..."
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
for w in (self._steer_delay, self._sw_delay, self._lane_turn, self._lane_speed):
|
||||
w.refresh()
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
189
iqpilot/selfdrive/ui/mici/layouts/settings/network/esim_ui.py
Normal file
189
iqpilot/selfdrive/ui/mici/layouts/settings/network/esim_ui.py
Normal file
@@ -0,0 +1,189 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from iqpilot.system.hardware.base import Profile
|
||||
from iqpilot.system.hardware.tici.esim_manager import EsimManager, EsimUiState, get_esim_manager
|
||||
from iqpilot.system.ui.lib.application import gui_app
|
||||
from iqpilot.system.ui.widgets import DialogResult, NavWidget
|
||||
from iqpilot.system.ui.widgets.esim_scanner import EsimQrScannerDialog
|
||||
from iqpilot.selfdrive.ui.mici.widgets.button import NeonBigButton
|
||||
from iqpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigInputDialog, BigMultiOptionDialog, BigConfirmationDialogV2
|
||||
from iqpilot.system.ui.widgets.scroller import Scroller
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
|
||||
|
||||
class EsimUIMici(NavWidget):
|
||||
def __init__(self, back_callback: Callable):
|
||||
super().__init__()
|
||||
self._manager: EsimManager = get_esim_manager()
|
||||
self._state = EsimUiState()
|
||||
self._callback_registered = False
|
||||
self._scroller = Scroller([], snap_items=False)
|
||||
self._rebuild_scroller()
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
if not self._callback_registered:
|
||||
self._manager.add_callback(self._on_state_update)
|
||||
self._callback_registered = True
|
||||
self._manager.refresh_profiles()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
if self._callback_registered:
|
||||
self._manager.remove_callback(self._on_state_update)
|
||||
self._callback_registered = False
|
||||
|
||||
def _is_busy(self) -> bool:
|
||||
return self._state.busy
|
||||
|
||||
def _status_text(self) -> str:
|
||||
if self._state.message:
|
||||
return self._state.message
|
||||
return self._state.state.value
|
||||
|
||||
def _on_state_update(self, state: EsimUiState):
|
||||
self._state = state
|
||||
self._rebuild_scroller()
|
||||
|
||||
def _show_choice_dialog(self, title: str, options: list[str], callback: Callable[[str], None]) -> None:
|
||||
if not options:
|
||||
return
|
||||
dlg = BigMultiOptionDialog(
|
||||
options,
|
||||
options[0],
|
||||
right_btn="check",
|
||||
right_btn_callback=lambda: callback(dlg.get_selected_option()),
|
||||
)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _rebuild_scroller(self):
|
||||
widgets = []
|
||||
|
||||
status_btn = NeonBigButton(tr("status"), chips=[self._status_text()])
|
||||
status_btn.set_enabled(False)
|
||||
widgets.append(status_btn)
|
||||
|
||||
refresh_btn = NeonBigButton(tr("refresh profiles"))
|
||||
refresh_btn.set_enabled(lambda: not self._is_busy())
|
||||
refresh_btn.set_click_callback(lambda: self._manager.refresh_profiles())
|
||||
widgets.append(refresh_btn)
|
||||
|
||||
add_btn = NeonBigButton(tr("add profile"), chips=[tr("scan qr / enter code")])
|
||||
add_btn.set_enabled(lambda: not self._is_busy())
|
||||
add_btn.set_click_callback(self._on_add_profile)
|
||||
widgets.append(add_btn)
|
||||
|
||||
profiles = self._state.profiles or []
|
||||
for p in profiles:
|
||||
widgets.append(self._make_profile_button(p))
|
||||
|
||||
self._scroller = Scroller(widgets, snap_items=False)
|
||||
|
||||
def _make_profile_button(self, profile: Profile) -> NeonBigButton:
|
||||
title = profile.nickname if profile.nickname else profile.iccid
|
||||
provider = profile.provider or tr("provider unknown")
|
||||
value = f"{provider}{' • ' + tr('active') if profile.enabled else ''}"
|
||||
btn = NeonBigButton(title, chips=[value])
|
||||
btn.set_enabled(lambda: not self._is_busy())
|
||||
btn.set_click_callback(lambda profile=profile: self._on_profile_selected(profile))
|
||||
return btn
|
||||
|
||||
def _on_add_profile(self):
|
||||
options = [tr("scan qr"), tr("enter code")]
|
||||
|
||||
def _selected(option: str):
|
||||
if option == options[0]:
|
||||
self._scan_qr()
|
||||
elif option == options[1]:
|
||||
self._manual_entry()
|
||||
|
||||
self._show_choice_dialog(tr("add esim profile"), options, _selected)
|
||||
|
||||
def _scan_qr(self):
|
||||
scanner = EsimQrScannerDialog()
|
||||
self._manager.set_scanning_state(True)
|
||||
|
||||
def _done(result: int):
|
||||
self._manager.set_scanning_state(False)
|
||||
if result != DialogResult.CONFIRM or not scanner.code:
|
||||
return
|
||||
self._prompt_nickname_and_add(scanner.code)
|
||||
|
||||
gui_app.set_modal_overlay(scanner, _done)
|
||||
|
||||
def _manual_entry(self):
|
||||
dlg = BigInputDialog(tr("enter LPA activation code..."), "", minimum_length=1,
|
||||
confirm_callback=lambda code: self._prompt_nickname_and_add(code.strip()) if code.strip() else None)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _prompt_nickname_and_add(self, code: str):
|
||||
dlg = BigInputDialog(tr("optional nickname..."), "", minimum_length=0,
|
||||
confirm_callback=lambda nickname: self._manager.add_profile(code, nickname.strip() or None))
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _on_profile_selected(self, profile: Profile):
|
||||
options = []
|
||||
if not profile.enabled:
|
||||
options.append(tr("activate"))
|
||||
options.append(tr("rename"))
|
||||
if self._manager.is_comma_profile(profile.iccid):
|
||||
options.append(tr("remove comma psim"))
|
||||
elif not profile.enabled:
|
||||
options.append(tr("delete"))
|
||||
|
||||
def _selected(option: str):
|
||||
if option == tr("activate"):
|
||||
self._manager.switch_profile(profile.iccid)
|
||||
elif option == tr("rename"):
|
||||
self._rename_profile(profile)
|
||||
elif option == tr("remove comma psim"):
|
||||
self._remove_comma_profile()
|
||||
elif option == tr("delete"):
|
||||
self._manager.delete_profile(profile.iccid)
|
||||
|
||||
self._show_choice_dialog(tr("profile actions"), options, _selected)
|
||||
|
||||
def _rename_profile(self, profile: Profile):
|
||||
dlg = BigInputDialog(tr("rename profile..."), profile.nickname or "", minimum_length=1,
|
||||
confirm_callback=lambda nickname: self._manager.rename_profile(profile.iccid, nickname.strip()) if nickname.strip() else None)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _remove_comma_profile(self):
|
||||
dlg = BigDialog(
|
||||
tr("Warning"),
|
||||
tr("This will permanently wipe the Comma pSIM profile from the SIM."),
|
||||
right_btn="check",
|
||||
right_btn_callback=self._remove_comma_profile_final_warning,
|
||||
)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _remove_comma_profile_final_warning(self):
|
||||
dlg = BigDialog(
|
||||
tr("Final Warning"),
|
||||
tr("You must use your own eSIM profile after this. You cannot use Comma Prime again unless you buy a new SIM from comma."),
|
||||
right_btn="check",
|
||||
right_btn_callback=self._confirm_remove_comma_profile,
|
||||
)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _confirm_remove_comma_profile(self):
|
||||
dlg = BigConfirmationDialogV2(
|
||||
tr("slide to remove\ncomma psim"),
|
||||
"icons_mici/settings/network/new/trash.png",
|
||||
red=True,
|
||||
confirm_callback=self._manager.bootstrap,
|
||||
)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if not self._manager.is_supported():
|
||||
from iqpilot.system.ui.widgets.label import gui_label
|
||||
gui_label(rect, tr("Insert the original comma SIM card that came with the device to use eSIM"), 48, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
return
|
||||
self._scroller.render(rect)
|
||||
@@ -0,0 +1,233 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
|
||||
from iqpilot.system.ui.widgets.scroller import Scroller, draw_scroller_edge_fades, draw_scroller_page_slider
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.network.esim_ui import EsimUIMici
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, BigParamControl, BigMultiToggle
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_dialog import BigInputDialog
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.system.ui.lib.application import gui_app
|
||||
from iqpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
from iqpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType
|
||||
from iqpilot.system.hardware.tici.esim_manager import get_esim_manager
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
|
||||
|
||||
class NetworkPanelType(IntEnum):
|
||||
NONE = 0
|
||||
WIFI = 1
|
||||
ESIM = 2
|
||||
|
||||
|
||||
class NetworkLayoutMici(NavWidget):
|
||||
CALLBACK_INTERVAL_FRAMES = 3
|
||||
|
||||
def __init__(self, back_callback: Callable):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = NetworkPanelType.WIFI
|
||||
self._callback_frame = 0
|
||||
self._esim_profile_count: str | None = None
|
||||
self._esim_profile_frame = 0
|
||||
|
||||
self._wifi_manager = WifiManager()
|
||||
self._wifi_manager.set_active(False)
|
||||
self._wifi_ui = WifiUIMici(self._wifi_manager)
|
||||
self._esim_ui = EsimUIMici(back_callback=lambda: self._switch_to_panel(NetworkPanelType.NONE))
|
||||
self._esim_manager = get_esim_manager()
|
||||
|
||||
self._wifi_manager.add_callbacks(
|
||||
networks_updated=self._on_network_updated,
|
||||
)
|
||||
|
||||
# ******** Tethering ********
|
||||
def tethering_toggle_callback(checked: bool):
|
||||
self._tethering_toggle_btn.set_enabled(False)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_active(checked)
|
||||
|
||||
self._tethering_checked = False
|
||||
self._tethering_toggle_btn = BigButton(tr("tethering"), tr("disabled"))
|
||||
self._tethering_toggle_btn.set_click_callback(lambda: self._on_tethering_clicked(tethering_toggle_callback))
|
||||
|
||||
def tethering_password_callback(password: str):
|
||||
if password:
|
||||
self._wifi_manager.set_tethering_password(password)
|
||||
|
||||
def tethering_password_clicked():
|
||||
tethering_password = self._wifi_manager.tethering_password
|
||||
dlg = BigInputDialog(tr("enter password..."), tethering_password, minimum_length=8,
|
||||
confirm_callback=tethering_password_callback)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
self._tethering_password_btn = BigButton(tr("tethering password"))
|
||||
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
|
||||
|
||||
# ******** IP Address ********
|
||||
self._ip_address_btn = BigButton(tr("IP Address"), tr("Not connected"))
|
||||
|
||||
# ******** Network Metered ********
|
||||
self._metered_options = [tr("default"), tr("metered"), tr("unmetered")]
|
||||
|
||||
def network_metered_callback(value: str):
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
metered = {
|
||||
self._metered_options[0]: MeteredType.UNKNOWN,
|
||||
self._metered_options[1]: MeteredType.YES,
|
||||
self._metered_options[2]: MeteredType.NO
|
||||
}.get(value, MeteredType.UNKNOWN)
|
||||
self._wifi_manager.set_current_network_metered(metered)
|
||||
|
||||
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
|
||||
# TODO: disable when not connected
|
||||
self._network_metered_btn = BigMultiToggle(tr("network usage"), self._metered_options, select_callback=network_metered_callback)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
|
||||
wifi_button = BigButton(tr("wi-fi"))
|
||||
wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui))
|
||||
self._esim_button = BigButton(tr("eSIM"), tr("manage profiles"))
|
||||
self._esim_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.ESIM))
|
||||
self._esim_button.set_visible(lambda: self._esim_manager.is_supported())
|
||||
|
||||
# ******** Advanced settings ********
|
||||
# ******** Roaming toggle ********
|
||||
self._roaming_btn = BigParamControl(tr("enable roaming"), "GsmRoaming", toggle_callback=self._toggle_roaming)
|
||||
|
||||
# ******** APN settings ********
|
||||
self._apn_btn = BigButton(tr("apn settings"))
|
||||
self._apn_btn.set_click_callback(self._edit_apn)
|
||||
|
||||
# ******** Cellular metered toggle ********
|
||||
self._cellular_metered_btn = BigParamControl(tr("cellular metered"), "GsmMetered", toggle_callback=self._toggle_cellular_metered)
|
||||
|
||||
# Main scroller ----------------------------------
|
||||
self._scroller = Scroller([
|
||||
wifi_button,
|
||||
self._esim_button,
|
||||
self._network_metered_btn,
|
||||
self._tethering_toggle_btn,
|
||||
self._tethering_password_btn,
|
||||
# /* Advanced settings
|
||||
self._roaming_btn,
|
||||
self._apn_btn,
|
||||
self._cellular_metered_btn,
|
||||
# */
|
||||
self._ip_address_btn,
|
||||
], snap_items=False)
|
||||
|
||||
# Set initial config
|
||||
roaming_enabled = ui_state.params.get_bool("GsmRoaming")
|
||||
metered = ui_state.params.get_bool("GsmMetered")
|
||||
self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered)
|
||||
|
||||
# Set up back navigation
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
def _back_enabled(self) -> bool:
|
||||
# Only allow swipe-to-dismiss back to settings when no sub-panel (eSIM) is open.
|
||||
return self._current_panel == NetworkPanelType.NONE
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
# konn3kt has no managed cellular SIM, so always expose the GSM/APN settings.
|
||||
show_cell_settings = True
|
||||
self._wifi_manager.set_ipv4_forward(show_cell_settings)
|
||||
self._roaming_btn.set_visible(show_cell_settings)
|
||||
self._apn_btn.set_visible(show_cell_settings)
|
||||
self._cellular_metered_btn.set_visible(show_cell_settings)
|
||||
|
||||
self._esim_profile_frame += 1
|
||||
if self._esim_profile_frame % 30 == 0:
|
||||
esim_profiles = (self._esim_manager.get_state().profiles or []) if self._esim_manager.is_supported() else []
|
||||
count = f"{len(esim_profiles)} profiles"
|
||||
if count != self._esim_profile_count:
|
||||
self._esim_profile_count = count
|
||||
self._esim_button.set_value(count)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._current_panel = NetworkPanelType.NONE
|
||||
self._esim_profile_frame = 0
|
||||
self._esim_profile_count = None
|
||||
self._roaming_btn.refresh()
|
||||
self._cellular_metered_btn.refresh()
|
||||
self._scroller.show_event()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
if self._current_panel == NetworkPanelType.ESIM:
|
||||
self._esim_ui.hide_event()
|
||||
|
||||
def _toggle_roaming(self, checked: bool):
|
||||
self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered"))
|
||||
|
||||
def _edit_apn(self):
|
||||
def update_apn(apn: str):
|
||||
apn = apn.strip()
|
||||
if apn == "":
|
||||
ui_state.params.remove("GsmApn")
|
||||
else:
|
||||
ui_state.params.put("GsmApn", apn)
|
||||
|
||||
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), apn, ui_state.params.get_bool("GsmMetered"))
|
||||
|
||||
current_apn = ui_state.params.get("GsmApn") or ""
|
||||
dlg = BigInputDialog(tr("enter APN"), current_apn, minimum_length=0, confirm_callback=update_apn)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
def _toggle_cellular_metered(self, checked: bool):
|
||||
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked)
|
||||
|
||||
def _on_tethering_clicked(self, toggle_callback):
|
||||
self._tethering_checked = not self._tethering_checked
|
||||
self._tethering_toggle_btn.set_value(tr("enabled") if self._tethering_checked else tr("disabled"))
|
||||
toggle_callback(self._tethering_checked)
|
||||
|
||||
def _on_network_updated(self, networks: list[Network]):
|
||||
# Update tethering state
|
||||
tethering_active = self._wifi_manager.is_tethering_active()
|
||||
self._tethering_toggle_btn.set_enabled(True)
|
||||
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
|
||||
self._tethering_checked = tethering_active
|
||||
self._tethering_toggle_btn.set_value(tr("enabled") if tethering_active else tr("disabled"))
|
||||
|
||||
# Update IP address
|
||||
self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or tr("Not connected"))
|
||||
|
||||
# Update network metered
|
||||
self._network_metered_btn.set_value(
|
||||
{
|
||||
MeteredType.UNKNOWN: self._metered_options[0],
|
||||
MeteredType.YES: self._metered_options[1],
|
||||
MeteredType.NO: self._metered_options[2]
|
||||
}.get(self._wifi_manager.current_network_metered, self._metered_options[0]))
|
||||
|
||||
def _switch_to_panel(self, panel_type: NetworkPanelType):
|
||||
if panel_type == NetworkPanelType.ESIM:
|
||||
if not self._esim_manager.is_supported():
|
||||
return
|
||||
self._esim_ui.show_event()
|
||||
elif self._current_panel == NetworkPanelType.ESIM:
|
||||
self._esim_ui.hide_event()
|
||||
|
||||
self._current_panel = panel_type
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
rl.draw_rectangle_rec(rect, rl.BLACK)
|
||||
if self._callback_frame % self.CALLBACK_INTERVAL_FRAMES == 0:
|
||||
self._wifi_manager.process_callbacks()
|
||||
self._callback_frame += 1
|
||||
|
||||
if self._current_panel == NetworkPanelType.ESIM:
|
||||
self._esim_ui.render(rect)
|
||||
else:
|
||||
self._scroller.render(rect)
|
||||
draw_scroller_edge_fades(rect)
|
||||
draw_scroller_page_slider(self._scroller, rect)
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Small standalone check for the mici WiFi menu sort order.
|
||||
|
||||
Run with:
|
||||
uv run python selfdrive/ui/mici/layouts/settings/network/test_wifi_sort.py
|
||||
"""
|
||||
|
||||
from iqpilot.system.ui.lib.wifi_manager import Network, SecurityType, wifi_network_sort_key
|
||||
|
||||
|
||||
def _network(ssid: str, strength: int, connected: bool = False) -> Network:
|
||||
return Network(ssid, strength, connected, SecurityType.WPA2, True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
cafe = _network("cafe", 35)
|
||||
home = _network("home", 75)
|
||||
connected = _network("connected", 20, connected=True)
|
||||
missing_saved = _network("missing-saved", 90)
|
||||
zero_strength = _network("zero-strength", 0)
|
||||
|
||||
entries = [
|
||||
(cafe, False),
|
||||
(missing_saved, True),
|
||||
(zero_strength, False),
|
||||
(connected, False),
|
||||
(home, False),
|
||||
]
|
||||
|
||||
ordered = [network.ssid for network, missing in sorted(entries, key=lambda entry: wifi_network_sort_key(*entry))]
|
||||
assert ordered == ["connected", "home", "cafe", "missing-saved", "zero-strength"], ordered
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
443
iqpilot/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py
Normal file
443
iqpilot/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py
Normal file
@@ -0,0 +1,443 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
import math
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
|
||||
from iqpilot.common.swaglog import cloudlog
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_dialog import BigInputDialog, BigConfirmationDialog
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, LABEL_COLOR
|
||||
from iqpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
from iqpilot.system.ui.widgets.scroller import NavScroller
|
||||
from iqpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, normalize_ssid, wifi_network_sort_key
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
|
||||
|
||||
def _wifi_network_signature(networks: list[Network]) -> tuple[tuple[str, int, bool, int, bool], ...]:
|
||||
return tuple(sorted((n.ssid, n.strength, n.is_connected, int(n.security_type), n.is_saved) for n in networks))
|
||||
|
||||
|
||||
class LoadingAnimation(Widget):
|
||||
RADIUS = 8
|
||||
SPACING = 24
|
||||
Y_MAG = 11.2
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, self.SPACING * 2 + self.RADIUS * 2, self.RADIUS * 2 + int(self.Y_MAG)))
|
||||
|
||||
def _render(self, _):
|
||||
base_x = int(self._rect.x + self._rect.width / 2)
|
||||
base_y = int(self._rect.y + self._rect.height - self.RADIUS)
|
||||
for i in range(3):
|
||||
x = base_x + (i - 1) * self.SPACING
|
||||
y = int(base_y + min(math.sin((rl.get_time() - i * 0.2) * 4) * self.Y_MAG, 0))
|
||||
alpha = int(np.interp(base_y - y, [0, self.Y_MAG], [255 * 0.45, 255 * 0.9]))
|
||||
rl.draw_circle(x, y, self.RADIUS, rl.Color(255, 255, 255, alpha))
|
||||
|
||||
|
||||
class WifiIcon(Widget):
|
||||
def __init__(self, network: Network):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, 48 + 5, 36 + 5))
|
||||
self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 48, 42)
|
||||
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 48, 36)
|
||||
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 48, 36)
|
||||
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 48, 36)
|
||||
self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 21, 27)
|
||||
self._network = network
|
||||
self._network_missing = False
|
||||
|
||||
def update_network(self, network: Network):
|
||||
self._network = network
|
||||
|
||||
def set_network_missing(self, missing: bool):
|
||||
self._network_missing = missing
|
||||
|
||||
def _render(self, _):
|
||||
strength = round(self._network.strength / 100 * 2)
|
||||
if self._network_missing:
|
||||
strength_icon = self._wifi_slash_txt
|
||||
elif strength == 2:
|
||||
strength_icon = self._wifi_full_txt
|
||||
elif strength == 1:
|
||||
strength_icon = self._wifi_medium_txt
|
||||
else:
|
||||
strength_icon = self._wifi_low_txt
|
||||
rl.draw_texture_ex(strength_icon, (self._rect.x, self._rect.y + self._rect.height - strength_icon.height), 0.0, 1.0, rl.WHITE)
|
||||
if self._network.security_type not in (SecurityType.OPEN, SecurityType.UNSUPPORTED):
|
||||
lock_x = self._rect.x + self._rect.width - self._lock_txt.width
|
||||
lock_y = self._rect.y + self._rect.height - self._lock_txt.height + 6
|
||||
rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
|
||||
class ForgetButton(Widget):
|
||||
MARGIN = 12
|
||||
|
||||
def __init__(self, forget_network: Callable):
|
||||
super().__init__()
|
||||
self._forget_network = forget_network
|
||||
self._bg_txt = gui_app.texture("icons_mici/settings/network/new/forget_button.png", 84, 84)
|
||||
self._bg_pressed_txt = gui_app.texture("icons_mici/settings/network/new/forget_button_pressed.png", 84, 84)
|
||||
self._trash_txt = gui_app.texture("icons_mici/settings/network/new/trash.png", 29, 35)
|
||||
self.set_rect(rl.Rectangle(0, 0, 84 + self.MARGIN * 2, 84 + self.MARGIN * 2))
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
dlg = BigConfirmationDialog(tr("slide to\nforget"), gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64),
|
||||
self._forget_network, red=True)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
def _render(self, _):
|
||||
bg_txt = self._bg_pressed_txt if self.is_pressed else self._bg_txt
|
||||
rl.draw_texture_ex(bg_txt, (self._rect.x + (self._rect.width - self._bg_txt.width) / 2,
|
||||
self._rect.y + (self._rect.height - self._bg_txt.height) / 2), 0, 1.0, rl.WHITE)
|
||||
trash_x = self._rect.x + (self._rect.width - self._trash_txt.width) / 2
|
||||
trash_y = self._rect.y + (self._rect.height - self._trash_txt.height) / 2
|
||||
rl.draw_texture_ex(self._trash_txt, (trash_x, trash_y), 0, 1.0, rl.WHITE)
|
||||
|
||||
|
||||
class DisconnectButton(Widget):
|
||||
MARGIN = 12
|
||||
RADIUS = 42
|
||||
# the only round button art is the destructive red one, and dropping a connection is not destructive
|
||||
BG = rl.Color(56, 56, 61, 255)
|
||||
BG_PRESSED = rl.Color(84, 84, 90, 255)
|
||||
|
||||
def __init__(self, disconnect_network: Callable):
|
||||
super().__init__()
|
||||
self._disconnect_network = disconnect_network
|
||||
self._slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 38, 38)
|
||||
self.set_rect(rl.Rectangle(0, 0, 84 + self.MARGIN * 2, 84 + self.MARGIN * 2))
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
dlg = BigConfirmationDialog(tr("slide to\ndisconnect"), gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 54, 54),
|
||||
self._disconnect_network)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
def _render(self, _):
|
||||
center = rl.Vector2(self._rect.x + self._rect.width / 2, self._rect.y + self._rect.height / 2)
|
||||
rl.draw_circle_v(center, self.RADIUS, self.BG_PRESSED if self.is_pressed else self.BG)
|
||||
rl.draw_texture_ex(self._slash_txt, (center.x - self._slash_txt.width / 2, center.y - self._slash_txt.height / 2),
|
||||
0, 1.0, rl.WHITE)
|
||||
|
||||
|
||||
class WifiButton(BigButton):
|
||||
LABEL_PADDING = 98
|
||||
LABEL_WIDTH = 402 - 98 - 28
|
||||
SUB_LABEL_WIDTH = 402 - BigButton.LABEL_HORIZONTAL_PADDING * 2
|
||||
|
||||
def __init__(self, network: Network, wifi_manager: WifiManager, connecting_ssid: Callable[[], str | None]):
|
||||
super().__init__(normalize_ssid(network.ssid), scroll=True)
|
||||
self._network = network
|
||||
self._wifi_manager = wifi_manager
|
||||
self._connecting_ssid = connecting_ssid
|
||||
self._wifi_icon = WifiIcon(network)
|
||||
self._forget_btn = ForgetButton(self._forget_network)
|
||||
self._disconnect_btn = DisconnectButton(self._disconnect_network)
|
||||
self._check_txt = gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 32, 32)
|
||||
self._network_missing = False
|
||||
self._network_forgetting = False
|
||||
self._network_disconnecting = False
|
||||
self._wrong_password = False
|
||||
|
||||
@property
|
||||
def network(self) -> Network:
|
||||
return self._network
|
||||
|
||||
def update_network(self, network: Network):
|
||||
self._network = network
|
||||
self._wifi_icon.update_network(network)
|
||||
self._network_missing = False
|
||||
self._wifi_icon.set_network_missing(False)
|
||||
if self._is_connected or self._is_connecting:
|
||||
self._wrong_password = False
|
||||
|
||||
@property
|
||||
def network_forgetting(self) -> bool:
|
||||
return self._network_forgetting
|
||||
|
||||
@property
|
||||
def network_missing(self) -> bool:
|
||||
return self._network_missing
|
||||
|
||||
def _forget_network(self):
|
||||
if self._network_forgetting:
|
||||
return
|
||||
self._network_forgetting = True
|
||||
self._wifi_manager.forget_connection(self._network.ssid)
|
||||
|
||||
def _disconnect_network(self):
|
||||
if self._network_disconnecting:
|
||||
return
|
||||
self._network_disconnecting = True
|
||||
self._wifi_manager.disconnect_connection(self._network.ssid)
|
||||
|
||||
def on_forgotten(self):
|
||||
self._network_forgetting = False
|
||||
|
||||
def on_disconnected(self):
|
||||
self._network_disconnecting = False
|
||||
|
||||
def set_network_missing(self, missing: bool):
|
||||
self._network_missing = missing
|
||||
self._wifi_icon.set_network_missing(missing)
|
||||
|
||||
def set_wrong_password(self):
|
||||
self._wrong_password = True
|
||||
self.trigger_shake()
|
||||
|
||||
@property
|
||||
def _is_saved(self) -> bool:
|
||||
return self._network.is_saved
|
||||
|
||||
@property
|
||||
def _is_connecting(self) -> bool:
|
||||
return self._connecting_ssid() == self._network.ssid
|
||||
|
||||
@property
|
||||
def _is_connected(self) -> bool:
|
||||
return self._network.is_connected
|
||||
|
||||
@property
|
||||
def _is_tethering(self) -> bool:
|
||||
return getattr(self._network, "is_tethering", False)
|
||||
|
||||
@property
|
||||
def _show_forget_btn(self) -> bool:
|
||||
if self._is_tethering or self._network_forgetting or self._show_disconnect_btn:
|
||||
return False
|
||||
return (self._is_saved and not self._wrong_password) or self._is_connecting
|
||||
|
||||
@property
|
||||
def _show_disconnect_btn(self) -> bool:
|
||||
# 402 units of row cannot hold both buttons plus the status word, so the slot is contextual:
|
||||
# disconnect while connected, forget once it is only saved
|
||||
if self._is_tethering or self._network_forgetting or self._network_disconnecting:
|
||||
return False
|
||||
return self._is_connected
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if self._show_forget_btn and rl.check_collision_point_rec(mouse_pos, self._forget_btn.rect):
|
||||
return
|
||||
if self._show_disconnect_btn and rl.check_collision_point_rec(mouse_pos, self._disconnect_btn.rect):
|
||||
return
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
def _get_label_font_size(self):
|
||||
return 48
|
||||
|
||||
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
|
||||
super().set_touch_valid_callback(lambda: touch_callback() and not self._forget_btn.is_pressed and not self._disconnect_btn.is_pressed)
|
||||
self._forget_btn.set_touch_valid_callback(touch_callback)
|
||||
self._disconnect_btn.set_touch_valid_callback(touch_callback)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if any((self._network_missing, self._is_connecting, self._is_connected, self._network_forgetting,
|
||||
self._network_disconnecting, self._network.security_type == SecurityType.UNSUPPORTED)):
|
||||
self.set_enabled(False)
|
||||
self._sub_label.set_color(rl.Color(255, 255, 255, int(255 * 0.585)))
|
||||
self._sub_label.set_font_weight(FontWeight.ROMAN)
|
||||
if self._network_forgetting:
|
||||
self.set_value(tr("forgetting..."))
|
||||
elif self._network_disconnecting:
|
||||
self.set_value(tr("disconnecting..."))
|
||||
elif self._is_connecting:
|
||||
self.set_value(tr("starting...") if self._is_tethering else tr("connecting..."))
|
||||
elif self._is_connected:
|
||||
self.set_value(tr("tethering") if self._is_tethering else tr("connected"))
|
||||
elif self._network_missing:
|
||||
self.set_value(tr("not in range"))
|
||||
else:
|
||||
self.set_value(tr("unsupported"))
|
||||
else:
|
||||
self.set_value(tr("wrong password") if self._wrong_password else tr("connect"))
|
||||
self.set_enabled(True)
|
||||
self._sub_label.set_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
self._sub_label.set_font_weight(FontWeight.SEMI_BOLD)
|
||||
|
||||
def _draw_content(self, btn_x: float, btn_y: float, btn_width: float, btn_height: float):
|
||||
self._label.set_color(LABEL_COLOR)
|
||||
label_rect = rl.Rectangle(btn_x + self.LABEL_PADDING, btn_y + self.LABEL_VERTICAL_PADDING,
|
||||
self.LABEL_WIDTH, btn_height - self.LABEL_VERTICAL_PADDING * 2)
|
||||
self._label.render(label_rect)
|
||||
|
||||
if self.value:
|
||||
sub_label_x = self._rect.x + self.LABEL_HORIZONTAL_PADDING
|
||||
label_y = btn_y + self._rect.height - self.LABEL_VERTICAL_PADDING
|
||||
sub_label_w = self.SUB_LABEL_WIDTH - (self._forget_btn.rect.width if self._show_forget_btn else 0) \
|
||||
- (self._disconnect_btn.rect.width if self._show_disconnect_btn else 0)
|
||||
sub_label_height = self._sub_label.get_content_height(sub_label_w)
|
||||
if self._is_connected and not self._network_forgetting and not self._network_disconnecting:
|
||||
check_y = int(label_y - sub_label_height + (sub_label_height - self._check_txt.height) / 2)
|
||||
rl.draw_texture_ex(self._check_txt, rl.Vector2(sub_label_x, check_y), 0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)))
|
||||
sub_label_x += self._check_txt.width + 14
|
||||
sub_label_rect = rl.Rectangle(sub_label_x, label_y - sub_label_height, sub_label_w, sub_label_height)
|
||||
self._sub_label.render(sub_label_rect)
|
||||
|
||||
self._wifi_icon.render(rl.Rectangle(self._rect.x + 30, btn_y + 30, self._wifi_icon.rect.width, self._wifi_icon.rect.height))
|
||||
|
||||
btn_right = self._rect.x + self._rect.width
|
||||
if self._show_forget_btn:
|
||||
self._forget_btn.render(rl.Rectangle(
|
||||
btn_right - self._forget_btn.rect.width,
|
||||
btn_y + self._rect.height - self._forget_btn.rect.height,
|
||||
self._forget_btn.rect.width, self._forget_btn.rect.height))
|
||||
btn_right -= self._forget_btn.rect.width
|
||||
|
||||
if self._show_disconnect_btn:
|
||||
self._disconnect_btn.render(rl.Rectangle(
|
||||
btn_right - self._disconnect_btn.rect.width,
|
||||
btn_y + self._rect.height - self._disconnect_btn.rect.height,
|
||||
self._disconnect_btn.rect.width, self._disconnect_btn.rect.height))
|
||||
|
||||
|
||||
class ScanningButton(BigButton):
|
||||
def __init__(self, is_scanning: Callable[[], bool]):
|
||||
super().__init__("", tr("searching for networks"))
|
||||
self.set_enabled(False)
|
||||
self._loading_animation = LoadingAnimation()
|
||||
self._is_scanning = is_scanning
|
||||
|
||||
def _draw_content(self, btn_x: float, btn_y: float, btn_width: float, btn_height: float):
|
||||
super()._draw_content(btn_x, btn_y, btn_width, btn_height)
|
||||
if not self._is_scanning():
|
||||
return
|
||||
anim = self._loading_animation
|
||||
anim.set_position(btn_x + btn_width - anim.rect.width - 40, btn_y + btn_height - anim.rect.height - 30)
|
||||
anim.render()
|
||||
|
||||
|
||||
class WifiUIMici(NavScroller):
|
||||
CALLBACK_INTERVAL_FRAMES = 3
|
||||
|
||||
def __init__(self, wifi_manager: WifiManager):
|
||||
super().__init__()
|
||||
self._wifi_manager = wifi_manager
|
||||
self._scanning_btn = ScanningButton(lambda: self._wifi_manager.is_scanning)
|
||||
self._networks: dict[str, Network] = {}
|
||||
self._network_signature: tuple[tuple[str, int, bool, int, bool], ...] = ()
|
||||
self._connecting: str | None = None
|
||||
self._callback_frame = 0
|
||||
self._wifi_manager.add_callbacks(
|
||||
need_auth=self._on_need_auth,
|
||||
activated=self._on_activated,
|
||||
forgotten=self._on_forgotten,
|
||||
networks_updated=self._on_network_updated,
|
||||
disconnected=self._on_disconnected,
|
||||
)
|
||||
|
||||
def _connecting_ssid(self) -> str | None:
|
||||
return self._connecting
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._wifi_manager.set_active(True)
|
||||
self._callback_frame = 0
|
||||
self._update_buttons(re_sort=True)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._wifi_manager.set_active(False)
|
||||
|
||||
def _on_network_updated(self, networks: list[Network]):
|
||||
signature = _wifi_network_signature(networks)
|
||||
if signature == self._network_signature:
|
||||
return
|
||||
self._network_signature = signature
|
||||
self._networks = {n.ssid: n for n in networks}
|
||||
self._update_buttons()
|
||||
|
||||
def _on_activated(self):
|
||||
self._connecting = None
|
||||
|
||||
def _on_disconnected(self):
|
||||
self._connecting = None
|
||||
for btn in self._scroller.items:
|
||||
if isinstance(btn, WifiButton):
|
||||
btn.on_disconnected()
|
||||
|
||||
def _on_forgotten(self, ssid=None):
|
||||
self._connecting = None
|
||||
for btn in self._scroller.items:
|
||||
if isinstance(btn, WifiButton) and (ssid is None or btn.network.ssid == ssid):
|
||||
btn.on_forgotten()
|
||||
|
||||
def _update_buttons(self, re_sort: bool = False):
|
||||
sorted_networks = sorted(self._networks.values(), key=wifi_network_sort_key)
|
||||
existing = {btn.network.ssid: btn for btn in self._scroller.items if isinstance(btn, WifiButton)}
|
||||
for network in sorted_networks:
|
||||
if network.ssid in existing:
|
||||
existing[network.ssid].update_network(network)
|
||||
else:
|
||||
btn = WifiButton(network, self._wifi_manager, self._connecting_ssid)
|
||||
btn.set_click_callback(lambda ssid=network.ssid: self._connect_to_network(ssid))
|
||||
self._scroller.add_widget(btn)
|
||||
|
||||
current_ssids = set(self._networks)
|
||||
for btn in self._scroller.items:
|
||||
if isinstance(btn, WifiButton):
|
||||
btn.set_network_missing(btn.network.ssid not in current_ssids)
|
||||
|
||||
if re_sort or sorted_networks:
|
||||
order = {btn.network.ssid: idx for idx, btn in enumerate(self._scroller.items) if isinstance(btn, WifiButton)}
|
||||
wifi_buttons = [btn for btn in self._scroller.items if isinstance(btn, WifiButton)]
|
||||
other_items = [btn for btn in self._scroller.items if not isinstance(btn, WifiButton) and btn is not self._scanning_btn]
|
||||
wifi_buttons.sort(key=lambda btn: (*wifi_network_sort_key(btn.network, btn.network_missing), order[btn.network.ssid]))
|
||||
self._scroller.items[:] = [*wifi_buttons, *other_items]
|
||||
|
||||
items = self._scroller.items
|
||||
if self._scanning_btn in items:
|
||||
items.append(items.pop(items.index(self._scanning_btn)))
|
||||
else:
|
||||
self._scroller.add_widget(self._scanning_btn)
|
||||
|
||||
def _connect_with_password(self, ssid: str, password: str):
|
||||
self._connecting = ssid
|
||||
self._wifi_manager.connect_to_network(ssid, password)
|
||||
self._move_network_to_front(ssid)
|
||||
|
||||
def _connect_to_network(self, ssid: str):
|
||||
network = self._networks.get(ssid)
|
||||
if network is None:
|
||||
cloudlog.warning(f"Trying to connect to unknown network: {ssid}")
|
||||
return
|
||||
if network.is_saved:
|
||||
self._connecting = ssid
|
||||
self._wifi_manager.activate_connection(ssid)
|
||||
elif network.security_type == SecurityType.OPEN:
|
||||
self._connecting = ssid
|
||||
self._wifi_manager.connect_to_network(ssid, "")
|
||||
else:
|
||||
self._on_need_auth(ssid, False)
|
||||
return
|
||||
self._move_network_to_front(ssid)
|
||||
|
||||
def _on_need_auth(self, ssid, incorrect_password=True):
|
||||
if incorrect_password:
|
||||
self._connecting = None
|
||||
for btn in self._scroller.items:
|
||||
if isinstance(btn, WifiButton) and btn.network.ssid == ssid:
|
||||
btn.set_wrong_password()
|
||||
break
|
||||
return
|
||||
dlg = BigInputDialog(tr("enter password..."), "", minimum_length=8,
|
||||
confirm_callback=lambda _password: self._connect_with_password(ssid, _password))
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
def _move_network_to_front(self, ssid: str | None):
|
||||
idx = next((i for i, btn in enumerate(self._scroller.items)
|
||||
if isinstance(btn, WifiButton) and btn.network.ssid == ssid), None) if ssid else None
|
||||
if idx is not None and idx > 0:
|
||||
self._scroller.move_item(idx, 0)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if self._callback_frame % self.CALLBACK_INTERVAL_FRAMES == 0:
|
||||
self._wifi_manager.process_callbacks()
|
||||
self._callback_frame += 1
|
||||
131
iqpilot/selfdrive/ui/mici/layouts/settings/settings.py
Normal file
131
iqpilot/selfdrive/ui/mici/layouts/settings/settings.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.system.ui.widgets.scroller import NavScroller
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.steering import SteeringLayoutMici
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.cruise import CruiseLayoutMici
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.visuals import VisualsLayoutMici
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.models import ModelsLayoutMici
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.display import DisplayLayoutMici
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.drive_history import TripsLayoutMici
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.vehicle import VehicleLayoutMici
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.dashcam import DashcamLayoutMici
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.network.network_layout import NetworkLayoutMici
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.software import SoftwareLayoutMici
|
||||
from iqpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
|
||||
|
||||
class SettingsBigButton(BigButton):
|
||||
def _get_label_font_size(self):
|
||||
return 64
|
||||
|
||||
|
||||
class CruiseModeButton(SettingsBigButton):
|
||||
"""Cruise menu button whose icon reflects the active longitudinal mode."""
|
||||
_ICON_SIZE = 60
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(tr("cruise"), "", gui_app.texture("icons_mici/speedometer.png", self._ICON_SIZE, self._ICON_SIZE))
|
||||
self._p = Params()
|
||||
self._icons = [
|
||||
gui_app.texture("icons_mici/speedometer.png", self._ICON_SIZE, self._ICON_SIZE),
|
||||
gui_app.texture("icons_mici/iqstandard_mode_mici.png", self._ICON_SIZE, self._ICON_SIZE),
|
||||
gui_app.texture("icons_mici/iqdynamic_mode_mici.png", self._ICON_SIZE, self._ICON_SIZE),
|
||||
gui_app.texture("icons_mici/experimental_mode_mici.png", self._ICON_SIZE, self._ICON_SIZE),
|
||||
]
|
||||
|
||||
def _mode_index(self) -> int:
|
||||
if not self._p.get_bool("AlphaLongitudinalEnabled"):
|
||||
return 0
|
||||
if not self._p.get_bool("ExperimentalMode"):
|
||||
return 1
|
||||
return 2 if self._p.get_bool("IQDynamicMode") else 3
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
self.set_icon(self._icons[self._mode_index()])
|
||||
|
||||
|
||||
class SettingsLayout(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
|
||||
toggles_panel = TogglesLayoutMici()
|
||||
toggles_btn = SettingsBigButton(tr("toggles"), "", gui_app.texture("icons_mici/settings.png", 64, 64))
|
||||
toggles_btn.set_click_callback(lambda: gui_app.push_widget(toggles_panel))
|
||||
|
||||
steering_panel = SteeringLayoutMici()
|
||||
steering_btn = SettingsBigButton(tr("steering"), "", gui_app.texture("icons_mici/wheel.png", 64, 64))
|
||||
steering_btn.set_click_callback(lambda: gui_app.push_widget(steering_panel))
|
||||
|
||||
cruise_panel = CruiseLayoutMici()
|
||||
cruise_btn = CruiseModeButton()
|
||||
cruise_btn.set_click_callback(lambda: gui_app.push_widget(cruise_panel))
|
||||
|
||||
visuals_panel = VisualsLayoutMici()
|
||||
visuals_btn = SettingsBigButton(tr("visuals"), "", gui_app.texture("icons_mici/onroad/eye_fill.png", 64, 46))
|
||||
visuals_btn.set_click_callback(lambda: gui_app.push_widget(visuals_panel))
|
||||
|
||||
models_panel = ModelsLayoutMici()
|
||||
models_btn = SettingsBigButton(tr("models"), "", gui_app.texture("icons_mici/models.png", 60, 60))
|
||||
models_btn.set_click_callback(lambda: gui_app.push_widget(models_panel))
|
||||
|
||||
display_panel = DisplayLayoutMici()
|
||||
display_btn = SettingsBigButton(tr("display"), "", gui_app.texture("icons_mici/settings/brightness.png", 62, 62))
|
||||
display_btn.set_click_callback(lambda: gui_app.push_widget(display_panel))
|
||||
|
||||
trips_panel = TripsLayoutMici()
|
||||
trips_btn = SettingsBigButton(tr("trips"), "", gui_app.texture("icons_mici/settings/trips.png", 62, 56))
|
||||
trips_btn.set_click_callback(lambda: gui_app.push_widget(trips_panel))
|
||||
|
||||
vehicle_panel = VehicleLayoutMici()
|
||||
vehicle_btn = SettingsBigButton(tr("vehicle"), "", gui_app.texture("icons_mici/settings/vehicle.png", 70, 56))
|
||||
vehicle_btn.set_click_callback(lambda: gui_app.push_widget(vehicle_panel))
|
||||
|
||||
dashcam_panel = DashcamLayoutMici()
|
||||
dashcam_btn = SettingsBigButton(tr("dashcam"), "", gui_app.texture("icons_mici/settings/camera.png", 64, 56))
|
||||
dashcam_btn.set_click_callback(lambda: gui_app.push_widget(dashcam_panel))
|
||||
|
||||
network_panel = NetworkLayoutMici(back_callback=lambda: gui_app.pop_widget())
|
||||
network_btn = SettingsBigButton(tr("network"), "", gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 76, 56))
|
||||
network_btn.set_click_callback(lambda: gui_app.push_widget(network_panel))
|
||||
|
||||
|
||||
device_panel = DeviceLayoutMici()
|
||||
device_btn = SettingsBigButton(tr("device"), "", gui_app.texture("icons_mici/settings/device_icon.png", 72, 58))
|
||||
device_btn.set_click_callback(lambda: gui_app.push_widget(device_panel))
|
||||
|
||||
software_panel = SoftwareLayoutMici()
|
||||
software_btn = SettingsBigButton(tr("software"), "", gui_app.texture("icons_mici/settings/sd_card.png", 60, 72))
|
||||
software_btn.set_click_callback(lambda: gui_app.push_widget(software_panel))
|
||||
|
||||
developer_panel = DeveloperLayoutMici()
|
||||
developer_btn = SettingsBigButton(tr("developer"), "", gui_app.texture("icons_mici/settings/developer_icon.png", 64, 60))
|
||||
developer_btn.set_click_callback(lambda: gui_app.push_widget(developer_panel))
|
||||
|
||||
self._scroller.add_widgets([
|
||||
device_btn,
|
||||
network_btn,
|
||||
PairBigButton(),
|
||||
models_btn,
|
||||
software_btn,
|
||||
steering_btn,
|
||||
cruise_btn,
|
||||
visuals_btn,
|
||||
display_btn,
|
||||
dashcam_btn,
|
||||
vehicle_btn,
|
||||
toggles_btn,
|
||||
trips_btn,
|
||||
developer_btn,
|
||||
])
|
||||
|
||||
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
||||
366
iqpilot/selfdrive/ui/mici/layouts/settings/software.py
Normal file
366
iqpilot/selfdrive/ui/mici/layouts/settings/software.py
Normal file
@@ -0,0 +1,366 @@
|
||||
import os
|
||||
import threading
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
|
||||
from iqpilot.common.time_helpers import system_time_valid
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.device import EngagedConfirmationButton
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_dialog import BigDialog
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
from iqpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from iqpilot.system.ui.widgets.scroller import NavScroller
|
||||
|
||||
UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond
|
||||
|
||||
|
||||
def _split_description(desc: str) -> tuple[str, str, str, str] | None:
|
||||
# UpdaterCurrentDescription/UpdaterNewDescription format: "version / branch / commit / date"
|
||||
parts = [p.strip() for p in desc.split(" / ")]
|
||||
if len(parts) != 4:
|
||||
return None
|
||||
version, branch, commit, date = parts
|
||||
return version, branch, commit, date
|
||||
|
||||
|
||||
class UpdaterState(IntEnum):
|
||||
IDLE = 0
|
||||
WAITING_FOR_UPDATER = 1
|
||||
UPDATER_RESPONDING = 2
|
||||
|
||||
|
||||
class SoftwareInfoLayoutMici(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.set_rect(rl.Rectangle(0, 0, 360, 180))
|
||||
|
||||
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
|
||||
max_width = int(self._rect.width - 20)
|
||||
self._version_label = UnifiedLabel(tr("version"), 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False)
|
||||
self._version_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color,
|
||||
font_weight=FontWeight.ROMAN, wrap_text=False, scroll=True)
|
||||
|
||||
self._branch_label = UnifiedLabel(tr("branch"), 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False)
|
||||
self._branch_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color,
|
||||
font_weight=FontWeight.ROMAN, wrap_text=False, scroll=True)
|
||||
|
||||
def _update_state(self):
|
||||
desc = _split_description(ui_state.params.get("UpdaterCurrentDescription") or "")
|
||||
if desc is not None:
|
||||
version, branch, commit, date = desc
|
||||
self._version_text_label.set_text(f"{version} ({date})")
|
||||
self._branch_text_label.set_text(f"{branch} ({commit})")
|
||||
else:
|
||||
self._version_text_label.set_text(ui_state.params.get("Version") or "N/A")
|
||||
self._branch_text_label.set_text(ui_state.params.get("GitBranch") or "N/A")
|
||||
|
||||
def _render(self, _):
|
||||
self._version_label.set_position(self._rect.x + 20, self._rect.y - 10)
|
||||
self._version_label.render()
|
||||
|
||||
self._version_text_label.set_position(self._rect.x + 20, self._rect.y + 68 - 25)
|
||||
self._version_text_label.render()
|
||||
|
||||
self._branch_label.set_position(self._rect.x + 20, self._rect.y + 114 - 30)
|
||||
self._branch_label.render()
|
||||
|
||||
self._branch_text_label.set_position(self._rect.x + 20, self._rect.y + 161 - 25)
|
||||
self._branch_text_label.render()
|
||||
|
||||
|
||||
class CheckUpdateButton(BigButton):
|
||||
def __init__(self):
|
||||
self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75)
|
||||
self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
|
||||
super().__init__(tr("check for update"), "", self._txt_update_icon)
|
||||
self.set_press_effect_enabled(False)
|
||||
|
||||
self._waiting_for_updater_t: float | None = None
|
||||
self._hide_value_t: float | None = None
|
||||
self._state: UpdaterState = UpdaterState.IDLE
|
||||
|
||||
ui_state.add_offroad_transition_callback(self.offroad_transition)
|
||||
|
||||
def offroad_transition(self):
|
||||
if ui_state.is_offroad():
|
||||
self.set_enabled(True)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
if not system_time_valid():
|
||||
dlg = BigDialog("", tr("Please connect to Wi-Fi to update."))
|
||||
gui_app.push_widget(dlg)
|
||||
return
|
||||
|
||||
self.set_enabled(False)
|
||||
self._state = UpdaterState.WAITING_FOR_UPDATER
|
||||
self.set_icon(self._txt_update_icon)
|
||||
|
||||
def run():
|
||||
if self.get_value() == "download update":
|
||||
os.system("pkill -SIGHUP -f system.updated.updated")
|
||||
else:
|
||||
os.system("pkill -SIGUSR1 -f system.updated.updated")
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
|
||||
def set_value(self, value: str):
|
||||
super().set_value(value)
|
||||
if value:
|
||||
self.set_text("")
|
||||
else:
|
||||
self.set_text(tr("check for update"))
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
if ui_state.started:
|
||||
self.set_enabled(False)
|
||||
return
|
||||
|
||||
updater_state = ui_state.params.get("UpdaterState") or ""
|
||||
failed_count = ui_state.params.get("UpdateFailedCount") or 0
|
||||
failed = int(failed_count) > 0
|
||||
update_available = ui_state.params.get_bool("UpdateAvailable")
|
||||
fetch_available = ui_state.params.get_bool("UpdaterFetchAvailable")
|
||||
cur_desc = ui_state.params.get("UpdaterCurrentDescription") or ""
|
||||
new_desc = ui_state.params.get("UpdaterNewDescription") or ""
|
||||
|
||||
# Ignore a stale failure marker once the updater is idle and already agrees there is
|
||||
# nothing new to install. Otherwise mici can get stuck on "failed to update" forever
|
||||
# even after a later successful fetch.
|
||||
stale_failure = failed and updater_state == "idle" and not fetch_available and not update_available and cur_desc == new_desc
|
||||
|
||||
if self._state == UpdaterState.WAITING_FOR_UPDATER:
|
||||
self.set_rotate_icon(True)
|
||||
if updater_state != "idle":
|
||||
self._state = UpdaterState.UPDATER_RESPONDING
|
||||
|
||||
# Recover from updater not responding (time invalid shortly after boot)
|
||||
if self._waiting_for_updater_t is None:
|
||||
self._waiting_for_updater_t = rl.get_time()
|
||||
|
||||
if self._waiting_for_updater_t is not None and rl.get_time() - self._waiting_for_updater_t > UPDATER_TIMEOUT:
|
||||
self.set_rotate_icon(False)
|
||||
self.set_value(tr("updater failed\nto respond"))
|
||||
self._state = UpdaterState.IDLE
|
||||
self._hide_value_t = rl.get_time()
|
||||
|
||||
elif self._state == UpdaterState.UPDATER_RESPONDING:
|
||||
if updater_state == "idle":
|
||||
self.set_rotate_icon(False)
|
||||
self._state = UpdaterState.IDLE
|
||||
self._hide_value_t = rl.get_time()
|
||||
else:
|
||||
if self.get_value() != tr(updater_state):
|
||||
self.set_value(tr(updater_state))
|
||||
|
||||
elif self._state == UpdaterState.IDLE:
|
||||
self.set_rotate_icon(False)
|
||||
if failed and not stale_failure:
|
||||
self.set_enabled(True) # allow retry when failure came from updater param
|
||||
if self.get_value() != tr("failed to update"):
|
||||
self.set_value(tr("failed to update"))
|
||||
|
||||
elif fetch_available:
|
||||
self.set_enabled(True)
|
||||
if self.get_value() != tr("download update"):
|
||||
self.set_value(tr("download update"))
|
||||
|
||||
elif self._hide_value_t is not None:
|
||||
self.set_enabled(True)
|
||||
if self.get_value() == tr("checking..."):
|
||||
self.set_value(tr("up to date"))
|
||||
self.set_icon(self._txt_up_to_date_icon)
|
||||
|
||||
# Hide previous text after short amount of time (up to date or failed)
|
||||
if rl.get_time() - self._hide_value_t > 3.0:
|
||||
self._hide_value_t = None
|
||||
self.set_value("")
|
||||
self.set_icon(self._txt_update_icon)
|
||||
else:
|
||||
self.set_enabled(True)
|
||||
if self.get_value() != "":
|
||||
self.set_value("")
|
||||
|
||||
if self._state != UpdaterState.WAITING_FOR_UPDATER:
|
||||
self._waiting_for_updater_t = None
|
||||
|
||||
|
||||
class InstallUpdateButton(BigButton):
|
||||
def __init__(self):
|
||||
super().__init__(tr("install update"), "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70))
|
||||
self.set_press_effect_enabled(False)
|
||||
self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable"))
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
desc = _split_description(ui_state.params.get("UpdaterNewDescription") or "")
|
||||
value = f"{desc[0]} ({desc[1]})" if desc is not None else ""
|
||||
if self.get_value() != value:
|
||||
self.set_value(value)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
self.set_enabled(False)
|
||||
|
||||
def run():
|
||||
ui_state.params.put_bool("DoReboot", True)
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
|
||||
|
||||
class InstallModePage(NavScroller):
|
||||
MODES = [
|
||||
("download_only", "predownload only"),
|
||||
("download_and_install", "predownload + preinstall"),
|
||||
]
|
||||
|
||||
def __init__(self, on_select: Callable[[str], None]):
|
||||
super().__init__()
|
||||
|
||||
current_mode = ui_state.params.get("UpdaterInstallMode") or "download_and_install"
|
||||
check_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
|
||||
|
||||
buttons = []
|
||||
for mode, label in self.MODES:
|
||||
btn = BigButton(tr(label), "", check_icon if mode == current_mode else None, scroll=True)
|
||||
btn.set_click_callback(lambda m=mode: self.dismiss(lambda: on_select(m)))
|
||||
buttons.append(btn)
|
||||
self._scroller.add_widgets(buttons)
|
||||
|
||||
|
||||
class InstallModeButton(BigButton):
|
||||
MODE_LABELS = {
|
||||
"download_only": "predownload only",
|
||||
"download_and_install": "predownload + preinstall",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(tr("update\ninstall mode"), "")
|
||||
self.set_press_effect_enabled(False)
|
||||
self._label.set_font_size(40)
|
||||
self._label.set_line_height(0.95)
|
||||
self.set_click_callback(self._on_click)
|
||||
self.set_enabled(lambda: ui_state.is_offroad())
|
||||
|
||||
def _current_mode(self) -> str:
|
||||
mode = ui_state.params.get("UpdaterInstallMode") or "download_and_install"
|
||||
if mode not in self.MODE_LABELS:
|
||||
return "download_and_install"
|
||||
return mode
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
self.set_value(tr(self.MODE_LABELS[self._current_mode()]))
|
||||
|
||||
def _on_click(self):
|
||||
gui_app.push_widget(InstallModePage(self._on_select))
|
||||
|
||||
def _on_select(self, mode: str):
|
||||
ui_state.params.put("UpdaterInstallMode", mode)
|
||||
self.set_value(tr(self.MODE_LABELS[self._current_mode()]))
|
||||
|
||||
|
||||
class BranchSelectPage(NavScroller):
|
||||
def __init__(self, on_select: Callable[[str], None]):
|
||||
super().__init__()
|
||||
|
||||
params = ui_state.params
|
||||
current_git_branch = params.get("GitBranch") or ""
|
||||
branches_str = params.get("UpdaterAvailableBranches") or ""
|
||||
branches = [b for b in branches_str.split(",") if b]
|
||||
|
||||
for b in [current_git_branch, "devel-staging", "devel", "nightly", "nightly-dev", "master"]:
|
||||
if b in branches:
|
||||
branches.remove(b)
|
||||
branches.insert(0, b)
|
||||
|
||||
current_target = params.get("UpdaterTargetBranch") or ""
|
||||
check_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
|
||||
|
||||
buttons = []
|
||||
for branch in branches:
|
||||
btn = BigButton(branch, "", check_icon if branch == current_target else None, scroll=True)
|
||||
btn.set_click_callback(lambda b=branch: self.dismiss(lambda: on_select(b)))
|
||||
buttons.append(btn)
|
||||
self._scroller.add_widgets(buttons)
|
||||
|
||||
|
||||
class TargetBranchButton(BigButton):
|
||||
def __init__(self):
|
||||
super().__init__(tr("target branch"), ui_state.params.get("UpdaterTargetBranch") or "")
|
||||
self.set_press_effect_enabled(False)
|
||||
self.set_click_callback(self._on_click)
|
||||
self.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
|
||||
self.set_enabled(lambda: ui_state.is_offroad())
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
target = ui_state.params.get("UpdaterTargetBranch") or ""
|
||||
if self.get_value() != target:
|
||||
self.set_value(target)
|
||||
|
||||
def _on_click(self):
|
||||
gui_app.push_widget(BranchSelectPage(self._on_select))
|
||||
|
||||
def _on_select(self, branch: str):
|
||||
ui_state.params.put("UpdaterTargetBranch", branch)
|
||||
self.set_value(branch)
|
||||
os.system("pkill -SIGUSR1 -f system.updated.updated")
|
||||
|
||||
|
||||
class DisableUpdatesButton(BigButton):
|
||||
def __init__(self):
|
||||
super().__init__(tr("disable\nupdates"), tr("currently on"))
|
||||
self.set_enabled(lambda: ui_state.is_offroad())
|
||||
self.set_press_effect_enabled(False)
|
||||
self._label.set_font_size(40)
|
||||
self._label.set_line_height(0.95)
|
||||
self.set_click_callback(self._on_pressed)
|
||||
|
||||
def _on_pressed(self):
|
||||
disabled = ui_state.params.get_bool("DisableUpdates")
|
||||
ui_state.params.put_bool("DisableUpdates", not disabled)
|
||||
self._sync_from_params()
|
||||
|
||||
def _sync_from_params(self):
|
||||
disabled = ui_state.params.get_bool("DisableUpdates")
|
||||
self.set_text(tr("enable\nupdates") if disabled else tr("disable\nupdates"))
|
||||
self.set_value(tr("currently off") if disabled else tr("currently on"))
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
self._sync_from_params()
|
||||
|
||||
|
||||
class SoftwareLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def uninstall_openpilot_callback():
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
|
||||
uninstall_openpilot_btn = EngagedConfirmationButton(tr("uninstall IQ.Pilot"), tr("uninstall"),
|
||||
gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64),
|
||||
uninstall_openpilot_callback, exit_on_confirm=False)
|
||||
uninstall_openpilot_btn.set_press_effect_enabled(False)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
SoftwareInfoLayoutMici(),
|
||||
CheckUpdateButton(),
|
||||
InstallUpdateButton(),
|
||||
InstallModeButton(),
|
||||
DisableUpdatesButton(),
|
||||
TargetBranchButton(),
|
||||
uninstall_openpilot_btn,
|
||||
])
|
||||
151
iqpilot/selfdrive/ui/mici/layouts/settings/steering.py
Normal file
151
iqpilot/selfdrive/ui/mici/layouts/settings/steering.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from iqpilot.cereal import car
|
||||
from iqpilot.common.params import Params
|
||||
from iqpilot.selfdrive.controls.lib.helpers.lane_change import AutoLaneChangeMode
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, BigParamControl
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.iq_widgets import MappedParamToggle
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.system.ui.lib.application import gui_app
|
||||
from iqpilot.system.ui.widgets.scroller import NavScroller
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
|
||||
|
||||
def _aol_modes() -> list[str]:
|
||||
return [tr("stay engaged"), tr("standby"), tr("disengage")]
|
||||
|
||||
|
||||
class SabBrakeToggle(BigParamControl):
|
||||
"""Driver-intervention toggle backed by AolSteeringMode == 2."""
|
||||
def __init__(self):
|
||||
super().__init__(tr("Driver Intervention Handling"), "AolEnabled")
|
||||
|
||||
def refresh(self):
|
||||
self.set_checked(int(self.params.get("AolSteeringMode", return_default=True)) == 2)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos):
|
||||
super(BigParamControl, self)._handle_mouse_release(mouse_pos)
|
||||
enabled = self._checked
|
||||
current_mode = int(self.params.get("AolSteeringMode", return_default=True))
|
||||
if enabled:
|
||||
self.params.put("AolSteeringMode", 2)
|
||||
elif current_mode == 2:
|
||||
self.params.put("AolSteeringMode", 1)
|
||||
|
||||
|
||||
def _has_limited_sab_options() -> bool:
|
||||
brand = ""
|
||||
if ui_state.is_offroad():
|
||||
bundle = ui_state.params.get("CarPlatformBundle")
|
||||
if bundle:
|
||||
brand = bundle.get("brand", "")
|
||||
if not brand:
|
||||
brand = ui_state.CP.brand if ui_state.CP else ""
|
||||
return brand == "rivian"
|
||||
|
||||
|
||||
class SabSettingsPanel(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._main_cruise = BigParamControl(tr("Availability While Cruise Changes"), "AolMainCruiseAllowed")
|
||||
self._brake = SabBrakeToggle()
|
||||
self._mode = MappedParamToggle(tr("Brake Response Mode"), "AolSteeringMode",
|
||||
_aol_modes(), [0, 1, 2])
|
||||
self._steer_override = BigParamControl(tr("Pause While You Steer"), "AolPauseOnSteeringOverride")
|
||||
self._scroller.add_widgets([self._main_cruise, self._brake, self._mode, self._steer_override])
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
limited = _has_limited_sab_options()
|
||||
if limited:
|
||||
ui_state.params.remove("AolMainCruiseAllowed")
|
||||
ui_state.params.put_bool("AolUnifiedEngagementMode", True)
|
||||
ui_state.params.put("AolSteeringMode", 2)
|
||||
offroad = ui_state.is_offroad()
|
||||
for w in (self._main_cruise, self._brake, self._mode):
|
||||
w.refresh()
|
||||
w.set_enabled(offroad and not limited)
|
||||
self._steer_override.refresh()
|
||||
self._steer_override.set_enabled(offroad)
|
||||
|
||||
|
||||
class LaneChangePanel(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._timer = MappedParamToggle(tr("Auto Lane Change"), "IQLaneChangeTimer",
|
||||
[tr("off"), tr("nudge"), tr("no nudge"), "0.5 s", "1 s", "2 s", "3 s"],
|
||||
[-1, 0, 1, 2, 3, 4, 5])
|
||||
self._bsm_delay = BigParamControl(tr("Delay with Blind Spot"), "IQLaneChangeBsmDelay")
|
||||
self._continuous = BigParamControl(tr("Continuous Changes"), "LaneChangeContinuous")
|
||||
self._scroller.add_widgets([self._timer, self._bsm_delay, self._continuous])
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._timer.refresh()
|
||||
enable_bsm = bool(ui_state.CP and ui_state.CP.enableBsm)
|
||||
if not enable_bsm and ui_state.params.get_bool("IQLaneChangeBsmDelay"):
|
||||
ui_state.params.remove("IQLaneChangeBsmDelay")
|
||||
self._bsm_delay.refresh()
|
||||
self._bsm_delay.set_enabled(
|
||||
enable_bsm and int(ui_state.params.get("IQLaneChangeTimer", return_default=True)) > AutoLaneChangeMode.NUDGE
|
||||
)
|
||||
self._continuous.refresh()
|
||||
|
||||
|
||||
class SteeringLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._sab_panel = SabSettingsPanel()
|
||||
self._lc_panel = LaneChangePanel()
|
||||
|
||||
self._aol = BigParamControl(tr("AOL"), "AolEnabled", toggle_callback=self._on_aol_toggled)
|
||||
self._sab_settings_button = BigButton(tr("steering assistance behavior"))
|
||||
self._sab_settings_button.set_click_callback(lambda: gui_app.push_widget(self._sab_panel))
|
||||
self._lane_change = BigButton(tr("lane change"))
|
||||
self._lane_change.set_click_callback(lambda: gui_app.push_widget(self._lc_panel))
|
||||
self._nnff = BigParamControl(tr("Neural Net FF"), "NeuralNetworkFeedForward", toggle_callback=self._on_nnff_toggled)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
self._aol, self._sab_settings_button, self._lane_change,
|
||||
self._nnff,
|
||||
])
|
||||
|
||||
def _aol_mode_str(self) -> str:
|
||||
try:
|
||||
return _aol_modes()[int(ui_state.params.get("AolSteeringMode", return_default=True))]
|
||||
except (TypeError, ValueError, IndexError):
|
||||
return _aol_modes()[0]
|
||||
|
||||
def _on_aol_toggled(self, checked: bool):
|
||||
if checked:
|
||||
ui_state.params.put_bool("AolUnifiedEngagementMode", True)
|
||||
|
||||
def _on_nnff_toggled(self, checked: bool):
|
||||
return None
|
||||
|
||||
def _refresh(self):
|
||||
offroad = ui_state.is_offroad()
|
||||
self._aol.refresh()
|
||||
self._aol.set_value(self._aol_mode_str())
|
||||
self._nnff.refresh()
|
||||
|
||||
steering_supported = (ui_state.CP is not None and
|
||||
ui_state.CP.steerControlType != car.CarParams.SteerControlType.angle)
|
||||
if not steering_supported:
|
||||
ui_state.params.remove("NeuralNetworkFeedForward")
|
||||
self._nnff.refresh()
|
||||
|
||||
self._aol.set_enabled(offroad)
|
||||
self._sab_settings_button.set_enabled(offroad and self._aol._checked)
|
||||
self._nnff.set_enabled(offroad and steering_supported)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
self._refresh()
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._refresh()
|
||||
48
iqpilot/selfdrive/ui/mici/layouts/settings/toggles.py
Normal file
48
iqpilot/selfdrive/ui/mici/layouts/settings/toggles.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from iqpilot.system.ui.widgets.scroller import NavScroller
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigParamControl
|
||||
from iqpilot.system.ui.lib.application import gui_app
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
|
||||
|
||||
class TogglesLayoutMici(NavScroller):
|
||||
"""Equivalent to the BIG UI toggles page, minus cruise items (personality / speed limit /
|
||||
longitudinal control live in Cruise) and dashcam items (dashcam / driver-cam / mic live in Dashcam)."""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
ui_state.params.put_bool("OpenpilotEnabledToggle", True)
|
||||
|
||||
disengage = BigParamControl(tr("disengage on accelerator"), "DisengageOnAccelerator")
|
||||
ldw = BigParamControl(tr("lane departure warnings"), "IsLdwEnabled")
|
||||
is_metric = BigParamControl(tr("use metric units"), "IsMetric")
|
||||
auto_units = BigParamControl(tr("set units from location"), "IQAutoUnits", toggle_callback=self._auto_units_callback)
|
||||
|
||||
self._scroller.add_widgets([disengage, ldw, is_metric, auto_units])
|
||||
|
||||
self._refresh_toggles = (
|
||||
("DisengageOnAccelerator", disengage),
|
||||
("IsLdwEnabled", ldw),
|
||||
("IsMetric", is_metric),
|
||||
("IQAutoUnits", auto_units),
|
||||
)
|
||||
|
||||
if ui_state.params.get_bool("ShowDebugInfo"):
|
||||
gui_app.set_show_touches(True)
|
||||
gui_app.set_show_fps(True)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._update_toggles()
|
||||
|
||||
def _auto_units_callback(self, state: bool):
|
||||
if state:
|
||||
ui_state.params.remove("IQAutoUnitsRegion")
|
||||
|
||||
def _update_toggles(self):
|
||||
ui_state.update_params()
|
||||
for key, item in self._refresh_toggles:
|
||||
item.set_checked(ui_state.params.get_bool(key))
|
||||
217
iqpilot/selfdrive/ui/mici/layouts/settings/vehicle.py
Normal file
217
iqpilot/selfdrive/ui/mici/layouts/settings/vehicle.py
Normal file
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
import unicodedata
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from iqpilot.selfdrive.car.vehicle_catalog import load_catalog
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigButton, BigParamControl
|
||||
from iqpilot.selfdrive.ui.mici.layouts.settings.iq_widgets import MappedParamToggle
|
||||
from iqpilot.selfdrive.ui.ui_state import ui_state
|
||||
from iqpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from iqpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from iqpilot.system.ui.widgets import Widget
|
||||
from iqpilot.system.ui.widgets.scroller import NavScroller
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
|
||||
|
||||
def _ascii_safe(text: str) -> str:
|
||||
return unicodedata.normalize("NFD", text).encode("ascii", "ignore").decode("ascii")
|
||||
|
||||
|
||||
def _load_platforms() -> dict:
|
||||
try:
|
||||
return load_catalog()
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
class _PickerRow(Widget):
|
||||
HEIGHT = 92
|
||||
|
||||
def __init__(self, label: str, on_tap):
|
||||
super().__init__()
|
||||
self._label = label
|
||||
self._on_tap = on_tap
|
||||
self.set_rect(rl.Rectangle(0, 0, gui_app.width, self.HEIGHT))
|
||||
self._font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
self._on_tap(self._label)
|
||||
|
||||
def _render(self, _):
|
||||
color = rl.Color(255, 255, 255, 255) if self.is_pressed else rl.Color(255, 255, 255, 200)
|
||||
ts = measure_text_cached(self._font, self._label, 46)
|
||||
rl.draw_text_ex(self._font, self._label, rl.Vector2(self._rect.x + 44, self._rect.y + (self.HEIGHT - ts.y) / 2), 46, 0, color)
|
||||
|
||||
|
||||
class _VerticalPicker(NavScroller):
|
||||
def __init__(self, options: list[str], on_pick):
|
||||
super().__init__(horizontal=False, snap_items=False, pad_start=20, pad_end=20)
|
||||
self._on_pick = on_pick
|
||||
rows = [_PickerRow(o, self._pick) for o in options]
|
||||
for row in rows:
|
||||
row.set_touch_valid_callback(lambda: self._scroller.scroll_panel.is_touch_valid())
|
||||
self._scroller.add_widgets(rows)
|
||||
|
||||
def _pick(self, option: str):
|
||||
gui_app.pop_widget()
|
||||
self._on_pick(option)
|
||||
|
||||
|
||||
class VehicleLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._platforms = _load_platforms()
|
||||
|
||||
self._vehicle_btn = BigButton(tr("vehicle"))
|
||||
self._vehicle_btn.set_click_callback(self._on_vehicle_clicked)
|
||||
|
||||
self._toyota_long = BigParamControl(tr("enforce factory long."), "IQToyotaFactoryLong",
|
||||
toggle_callback=self._on_toyota_long)
|
||||
self._hyundai_tuning = MappedParamToggle(tr("hyundai long. tuning"), "IQHyundaiLongTune",
|
||||
[tr("off"), tr("dynamic"), tr("predictive")], [0, 1, 2])
|
||||
self._subaru_snag = BigParamControl(tr("creep from standstill (beta)"), "IQSubaruCreepAssist")
|
||||
self._subaru_manual = BigParamControl(tr("stop and go manual brake"), "IQSubaruCreepAssistManualBrake")
|
||||
self._vw_pq_hca = BigParamControl(tr("PQ HCA status 7 mode"), "pqhca5or7Toggle")
|
||||
self._vw_lateral = BigParamControl(tr("lateral when cruise faulted"), "AllowLateralWhenLongUnavailable")
|
||||
self._vw_mqb_acc_resume = BigParamControl(tr("MQB ACC resume"), "iqMqbAccResume")
|
||||
self._vw_mqb_steering_lockout = BigParamControl(tr("MQB steering lockout"), "iqMqbSteeringLockout")
|
||||
self._tesla_vtb = BigParamControl(tr("virtual torque blending"), "IQTeslaTorqueBlend")
|
||||
self._tesla_fsd_visualization = BigParamControl(tr("FSD visuals"), "IQTeslaFsdVisualization")
|
||||
|
||||
self._brand_widgets = {
|
||||
"toyota": [self._toyota_long],
|
||||
"hyundai": [self._hyundai_tuning],
|
||||
"subaru": [self._subaru_snag, self._subaru_manual],
|
||||
"volkswagen": [self._vw_pq_hca, self._vw_lateral, self._vw_mqb_acc_resume, self._vw_mqb_steering_lockout],
|
||||
"tesla": [self._tesla_vtb, self._tesla_fsd_visualization],
|
||||
}
|
||||
self._all_brand_widgets = [w for ws in self._brand_widgets.values() for w in ws]
|
||||
|
||||
self._scroller.add_widgets([self._vehicle_btn] + self._all_brand_widgets)
|
||||
|
||||
def _get_current_brand(self) -> str:
|
||||
bundle = ui_state.params.get("CarPlatformBundle")
|
||||
if bundle:
|
||||
return bundle.get("brand", "")
|
||||
if ui_state.CP:
|
||||
return getattr(ui_state.CP, "brand", "")
|
||||
return ""
|
||||
|
||||
def _vw_flags(self):
|
||||
try:
|
||||
from iqdbc.car.volkswagen.values import CAR
|
||||
bundle = ui_state.params.get("CarPlatformBundle")
|
||||
if bundle and (platform := bundle.get("platform")):
|
||||
return CAR[platform].config.flags
|
||||
if ui_state.CP:
|
||||
return ui_state.CP.flags
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
|
||||
def _uses_vw_hca_status_toggle(self) -> bool:
|
||||
from iqdbc.car.volkswagen.values import VolkswagenFlags
|
||||
return bool(self._vw_flags() & (VolkswagenFlags.PQ | VolkswagenFlags.MLB))
|
||||
|
||||
def _is_vw_mqb(self) -> bool:
|
||||
from iqdbc.car.volkswagen.values import VolkswagenFlags
|
||||
flags = self._vw_flags()
|
||||
return not bool(flags & (VolkswagenFlags.PQ | VolkswagenFlags.MLB | VolkswagenFlags.MEB | VolkswagenFlags.MEB_GEN2 | VolkswagenFlags.MQB_EVO))
|
||||
|
||||
def _supports_vw_lateral_when_faulted(self) -> bool:
|
||||
from iqdbc.car.volkswagen.values import VolkswagenFlags
|
||||
# PQ, MEB, MQB_EVO and base MQB all implement cruiseFaultLateralMode in carstate.py.
|
||||
# MLB does not.
|
||||
return not bool(self._vw_flags() & VolkswagenFlags.MLB)
|
||||
|
||||
def _pretty_name(self, platform: str) -> str:
|
||||
for name, v in self._platforms.items():
|
||||
if v.get("platform") == platform:
|
||||
make = v.get("make", "")
|
||||
if make and name.lower().startswith(make.lower() + " "):
|
||||
return name[len(make) + 1:]
|
||||
return name
|
||||
return _ascii_safe(platform).replace("_", " ").title()
|
||||
|
||||
def _vehicle_status(self) -> str:
|
||||
bundle = ui_state.params.get("CarPlatformBundle")
|
||||
if bundle:
|
||||
name = _ascii_safe(bundle.get("name", "?"))
|
||||
make = bundle.get("make", "")
|
||||
if make and name.lower().startswith(make.lower() + " "):
|
||||
name = name[len(make) + 1:]
|
||||
return name[:1].upper() + name[1:]
|
||||
if ui_state.CP and ui_state.CP.carFingerprint not in ("", "MOCK"):
|
||||
return self._pretty_name(ui_state.CP.carFingerprint)
|
||||
return "tap to select"
|
||||
|
||||
def _on_vehicle_clicked(self):
|
||||
if ui_state.params.get("CarPlatformBundle"):
|
||||
ui_state.params.remove("CarPlatformBundle")
|
||||
self._refresh()
|
||||
else:
|
||||
self._open_make_picker()
|
||||
|
||||
def _open_make_picker(self):
|
||||
makes = sorted({v.get("make", "") for v in self._platforms.values() if v.get("make")})
|
||||
label_to_make = {_ascii_safe(m): m for m in makes}
|
||||
gui_app.push_widget(_VerticalPicker(list(label_to_make.keys()),
|
||||
lambda lbl: self._open_model_picker(label_to_make.get(lbl, ""))))
|
||||
|
||||
def _open_model_picker(self, make: str):
|
||||
if not make:
|
||||
return
|
||||
prefix = make + " "
|
||||
label_to_key: dict = {}
|
||||
for key in sorted(p for p, v in self._platforms.items() if v.get("make") == make):
|
||||
label = key[len(prefix):] if key.lower().startswith(prefix.lower()) else key
|
||||
label_to_key[_ascii_safe(label)] = key
|
||||
gui_app.push_widget(_VerticalPicker(list(label_to_key.keys()),
|
||||
lambda lbl: self._select_platform(label_to_key.get(lbl, ""))))
|
||||
|
||||
def _select_platform(self, key: str):
|
||||
if key and (data := self._platforms.get(key)):
|
||||
ui_state.params.put("CarPlatformBundle", {**data, "name": key})
|
||||
gui_app.pop_widgets_to(self)
|
||||
self._refresh()
|
||||
|
||||
def _on_toyota_long(self, checked: bool):
|
||||
if checked and ui_state.params.get_bool("AlphaLongitudinalEnabled"):
|
||||
ui_state.params.put_bool("AlphaLongitudinalEnabled", False)
|
||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def _refresh(self):
|
||||
self._vehicle_btn.set_value(self._vehicle_status())
|
||||
|
||||
brand = self._get_current_brand()
|
||||
offroad = ui_state.is_offroad()
|
||||
uses_hca_status_toggle = self._uses_vw_hca_status_toggle()
|
||||
is_mqb = self._is_vw_mqb()
|
||||
supports_lateral_when_faulted = self._supports_vw_lateral_when_faulted()
|
||||
visible = set(self._brand_widgets.get(brand, []))
|
||||
for w in self._all_brand_widgets:
|
||||
show = w in visible
|
||||
if w is self._vw_pq_hca:
|
||||
show = show and uses_hca_status_toggle
|
||||
elif w in (self._vw_mqb_acc_resume, self._vw_mqb_steering_lockout):
|
||||
show = show and is_mqb
|
||||
elif w is self._vw_lateral:
|
||||
show = show and supports_lateral_when_faulted
|
||||
w.set_visible(show)
|
||||
if show:
|
||||
w.refresh()
|
||||
for w in (self._toyota_long, self._subaru_snag, self._subaru_manual, self._tesla_vtb, self._tesla_fsd_visualization):
|
||||
w.set_enabled(offroad)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
self._vehicle_btn.set_value(self._vehicle_status())
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._refresh()
|
||||
26
iqpilot/selfdrive/ui/mici/layouts/settings/visuals.py
Normal file
26
iqpilot/selfdrive/ui/mici/layouts/settings/visuals.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/
|
||||
"""
|
||||
|
||||
from iqpilot.selfdrive.ui.mici.widgets.stock_button import BigParamControl
|
||||
from iqpilot.system.ui.widgets.scroller import NavScroller
|
||||
from iqpilot.system.ui.lib.multilang import tr
|
||||
|
||||
|
||||
class VisualsLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._blind_spot = BigParamControl(tr("Blind Spot Warnings"), "IQBlindSpotAlerts")
|
||||
self._steering_arc = BigParamControl(tr("Steering Effort Arc"), "IQSteerEffortArc")
|
||||
self._road_name = BigParamControl(tr("Road Name"), "IQRoadNameOverlay")
|
||||
self._turn_signals = BigParamControl(tr("Turn Signals"), "IQBlinkerIndicators")
|
||||
self._accel_bar = BigParamControl(tr("Acceleration Bar"), "IQAccelMeter")
|
||||
|
||||
self._toggles = [self._blind_spot, self._steering_arc, self._road_name,
|
||||
self._turn_signals, self._accel_bar]
|
||||
self._scroller.add_widgets(self._toggles)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
for w in self._toggles:
|
||||
w.refresh()
|
||||
Reference in New Issue
Block a user