mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-03 12:32:06 +08:00
UI
This commit is contained in:
+103
-243
@@ -1,23 +1,17 @@
|
||||
import time
|
||||
import datetime
|
||||
import re
|
||||
import time
|
||||
|
||||
from cereal import log
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from openpilot.system.ui.widgets.label import gui_label, MiciLabel, UnifiedLabel
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR, MousePos
|
||||
from openpilot.starpilot.common.starpilot_variables import MODELS_PATH
|
||||
from openpilot.starpilot.common.experimental_state import (
|
||||
CEStatus,
|
||||
next_manual_ce_status,
|
||||
requested_experimental_mode,
|
||||
sync_manual_ce_state,
|
||||
)
|
||||
from openpilot.system.ui.widgets.layouts import HBoxLayout
|
||||
from openpilot.system.ui.widgets.icon_widget import IconWidget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.text import wrap_text
|
||||
from openpilot.system.version import training_version
|
||||
from openpilot.system.hardware import PC
|
||||
from openpilot.system.version import RELEASE_BRANCHES
|
||||
|
||||
HEAD_BUTTON_FONT_SIZE = 40
|
||||
HOME_PADDING = 8
|
||||
@@ -35,57 +29,56 @@ NETWORK_TYPES = {
|
||||
}
|
||||
|
||||
|
||||
class DeviceStatus(Widget):
|
||||
class NetworkIcon(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.set_rect(rl.Rectangle(0, 0, 54, 44)) # max size of all icons
|
||||
self._net_type = NetworkType.none
|
||||
self._net_strength = 0
|
||||
|
||||
self._do_welcome()
|
||||
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)
|
||||
|
||||
def _do_welcome(self):
|
||||
# Keep onboarding bypass for desktop UI runs only.
|
||||
if PC:
|
||||
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 = "starpilot"
|
||||
description = ui_state.params.get("UpdaterCurrentDescription")
|
||||
return f"{brand} {description}" if description else brand
|
||||
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)
|
||||
|
||||
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}"
|
||||
device_state = ui_state.sm['deviceState']
|
||||
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 _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)
|
||||
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)
|
||||
elif self._net_type in (NetworkType.cell2G, NetworkType.cell3G, NetworkType.cell4G, NetworkType.cell5G):
|
||||
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)
|
||||
else:
|
||||
draw_net_txt = self._wifi_slash_txt
|
||||
|
||||
# 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_x = self._rect.x + (self._rect.width - draw_net_txt.width) / 2
|
||||
draw_y = self._rect.y + (self._rect.height - draw_net_txt.height) / 2
|
||||
|
||||
# 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)
|
||||
if draw_net_txt == self._wifi_slash_txt:
|
||||
# Offset by difference in height between slashless and slash icons to make center align match
|
||||
draw_y -= (self._wifi_slash_txt.height - self._wifi_none_txt.height) / 2
|
||||
|
||||
rl.draw_texture_ex(draw_net_txt, rl.Vector2(draw_x, draw_y), 0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
|
||||
|
||||
class MiciHomeLayout(Widget):
|
||||
@@ -100,117 +93,46 @@ class MiciHomeLayout(Widget):
|
||||
|
||||
self._version_text = None
|
||||
self._experimental_mode = False
|
||||
self._safe_mode = False
|
||||
self._current_model_name = "default"
|
||||
|
||||
self._settings_txt = gui_app.texture("icons_mici/settings.png", 48, 48)
|
||||
self._experimental_txt = gui_app.texture("icons_mici/experimental_mode.png", 48, 48)
|
||||
self._mic_txt = gui_app.texture("icons_mici/microphone.png", 48, 48)
|
||||
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
|
||||
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
|
||||
|
||||
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
||||
self._net_strength = 0
|
||||
self._status_bar_layout = HBoxLayout([
|
||||
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
|
||||
NetworkIcon(),
|
||||
self._experimental_icon,
|
||||
self._mic_icon,
|
||||
], spacing=18)
|
||||
|
||||
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, 44)
|
||||
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 50, 44)
|
||||
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 50, 44)
|
||||
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 50, 44)
|
||||
|
||||
self._cell_none_txt = gui_app.texture("icons_mici/settings/network/cell_strength_none.png", 55, 35)
|
||||
self._cell_low_txt = gui_app.texture("icons_mici/settings/network/cell_strength_low.png", 55, 35)
|
||||
self._cell_medium_txt = gui_app.texture("icons_mici/settings/network/cell_strength_medium.png", 55, 35)
|
||||
self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 55, 35)
|
||||
self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 55, 35)
|
||||
|
||||
self._openpilot_label = MiciLabel("starpilot", font_size=96, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY)
|
||||
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._openpilot_label = UnifiedLabel("starpilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False)
|
||||
self._version_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||
self._large_version_label = UnifiedLabel("", font_size=64, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||
self._date_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||
self._branch_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, scroll=True)
|
||||
self._version_commit_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
self._version_commit_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._version_text = self._get_version_text()
|
||||
self._update_network_status(ui_state.sm['deviceState'])
|
||||
self._update_params()
|
||||
|
||||
def _update_params(self):
|
||||
self._safe_mode = ui_state.params.get_bool("SafeMode")
|
||||
self._experimental_mode = requested_experimental_mode(ui_state.params, ui_state.params_memory)
|
||||
self._experimental_mode = ui_state.params.get_bool("ExperimentalMode")
|
||||
|
||||
def _clean_name(value: str) -> str:
|
||||
def _clean_model_name(value: str) -> str:
|
||||
return re.sub(r"[🗺️👀📡]", "", value).replace("(Default)", "").strip()
|
||||
|
||||
def _decode_default(value) -> str:
|
||||
if isinstance(value, bytes):
|
||||
return value.decode("utf-8", errors="ignore").strip()
|
||||
return str(value or "").strip()
|
||||
current_name = _clean_model_name(ui_state.params.get("DrivingModelName", encoding="utf-8") or "")
|
||||
if not current_name:
|
||||
default_name = ui_state.params.get_default_value("DrivingModelName")
|
||||
if isinstance(default_name, bytes):
|
||||
default_name = default_name.decode("utf-8", errors="ignore")
|
||||
current_name = _clean_model_name(str(default_name or ""))
|
||||
|
||||
model_key = (ui_state.params.get("Model", encoding="utf-8") or
|
||||
ui_state.params.get("DrivingModel", encoding="utf-8") or "").strip()
|
||||
current_param_name = _clean_name(ui_state.params.get("DrivingModelName", encoding="utf-8") or "")
|
||||
|
||||
available_models = [entry.strip() for entry in (ui_state.params.get("AvailableModels", encoding="utf-8") or "").split(",")]
|
||||
available_names = [entry.strip() for entry in (ui_state.params.get("AvailableModelNames", encoding="utf-8") or "").split(",")]
|
||||
model_versions = [entry.strip() for entry in (ui_state.params.get("ModelVersions", encoding="utf-8") or "").split(",")]
|
||||
model_name_map = {
|
||||
key: _clean_name(name)
|
||||
for key, name in zip(available_models, available_names)
|
||||
if key and _clean_name(name)
|
||||
}
|
||||
model_version_map = {
|
||||
key: version
|
||||
for key, version in zip(available_models, model_versions)
|
||||
if key and version
|
||||
}
|
||||
|
||||
default_key = _decode_default(ui_state.params.get_default_value("DrivingModel") or
|
||||
ui_state.params.get_default_value("Model")) or "sc"
|
||||
default_name = _clean_name(_decode_default(ui_state.params.get_default_value("DrivingModelName"))) or "South Carolina"
|
||||
|
||||
def _is_model_installed(key: str) -> bool:
|
||||
if not key:
|
||||
return False
|
||||
|
||||
# Built-in default model is always available.
|
||||
if key == default_key:
|
||||
return True
|
||||
|
||||
if (MODELS_PATH / f"{key}.thneed").is_file():
|
||||
return True
|
||||
|
||||
version = model_version_map.get(key, "")
|
||||
required = [
|
||||
f"{key}_driving_policy_tinygrad.pkl",
|
||||
f"{key}_driving_vision_tinygrad.pkl",
|
||||
f"{key}_driving_policy_metadata.pkl",
|
||||
f"{key}_driving_vision_metadata.pkl",
|
||||
]
|
||||
if version == "v12":
|
||||
required.extend([
|
||||
f"{key}_driving_off_policy_tinygrad.pkl",
|
||||
f"{key}_driving_off_policy_metadata.pkl",
|
||||
])
|
||||
return all((MODELS_PATH / filename).is_file() for filename in required)
|
||||
|
||||
# If a stale custom model is selected but not actually installed, show default.
|
||||
if model_key and not _is_model_installed(model_key):
|
||||
model_key = default_key
|
||||
|
||||
resolved_name = ""
|
||||
if model_key in model_name_map:
|
||||
resolved_name = model_name_map[model_key]
|
||||
elif model_key.endswith("2") and model_key[:-1] in model_name_map:
|
||||
resolved_name = model_name_map[model_key[:-1]]
|
||||
elif model_key == default_key or (model_key.endswith("2") and model_key[:-1] == default_key):
|
||||
resolved_name = default_name
|
||||
|
||||
if not resolved_name and current_param_name:
|
||||
resolved_name = current_param_name
|
||||
if not resolved_name:
|
||||
resolved_name = default_name if (not model_key or model_key == default_key) else model_key
|
||||
|
||||
self._current_model_name = resolved_name
|
||||
current_key = (ui_state.params.get("Model", encoding="utf-8") or
|
||||
ui_state.params.get("DrivingModel", encoding="utf-8") or "").strip()
|
||||
self._current_model_name = current_name or current_key or "default"
|
||||
|
||||
def _update_state(self):
|
||||
if self.is_pressed and not self._is_pressed_prev:
|
||||
@@ -223,33 +145,18 @@ class MiciHomeLayout(Widget):
|
||||
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 and not self._safe_mode:
|
||||
if ui_state.params.get_bool("ConditionalExperimental"):
|
||||
current_status = ui_state.params_memory.get_int("CEStatus", default=CEStatus["OFF"])
|
||||
override_value = next_manual_ce_status(current_status, self._experimental_mode)
|
||||
ui_state.params_memory.put_int("CEStatus", override_value)
|
||||
sync_manual_ce_state(ui_state.params, override_value)
|
||||
self._experimental_mode = override_value == CEStatus["USER_OVERRIDDEN"]
|
||||
else:
|
||||
self._experimental_mode = not self._experimental_mode
|
||||
ui_state.params.put_bool("ExperimentalMode", self._experimental_mode)
|
||||
if ui_state.has_longitudinal_control:
|
||||
self._experimental_mode = not self._experimental_mode
|
||||
ui_state.params.put("ExperimentalMode", self._experimental_mode)
|
||||
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
|
||||
|
||||
@@ -260,17 +167,22 @@ class MiciHomeLayout(Widget):
|
||||
self._did_long_press = False
|
||||
|
||||
def _get_version_text(self) -> tuple[str, str, str, str] | None:
|
||||
description = ui_state.params.get("UpdaterCurrentDescription")
|
||||
version = ui_state.params.get("Version")
|
||||
branch = ui_state.params.get("GitBranch")
|
||||
commit = ui_state.params.get("GitCommit")
|
||||
|
||||
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
|
||||
if not all((version, branch, commit)):
|
||||
return None
|
||||
|
||||
return None
|
||||
commit_date_raw = ui_state.params.get("GitCommitDate")
|
||||
try:
|
||||
# GitCommitDate format from get_commit_date(): '%ct %ci' e.g. "'1708012345 2024-02-15 ...'"
|
||||
unix_ts = int(commit_date_raw.strip("'").split()[0])
|
||||
date_str = datetime.datetime.fromtimestamp(unix_ts).strftime("%b %d")
|
||||
except (ValueError, IndexError, TypeError, AttributeError):
|
||||
date_str = ""
|
||||
|
||||
return version, branch, commit[:7], date_str
|
||||
|
||||
def _render(self, _):
|
||||
# TODO: why is there extra space here to get it to be flush?
|
||||
@@ -279,83 +191,31 @@ class MiciHomeLayout(Widget):
|
||||
self._openpilot_label.render()
|
||||
|
||||
if self._version_text is not None:
|
||||
# release branch
|
||||
release_branch = self._version_text[1] in RELEASE_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.set_position(version_pos.x + self._version_label.text_width + 10, version_pos.y)
|
||||
self._date_label.render()
|
||||
|
||||
self._branch_label.set_max_width(gui_app.width - self._version_label.rect.width - self._date_label.rect.width - 32)
|
||||
self._branch_label.set_max_width(gui_app.width - self._version_label.text_width - self._date_label.text_width - 32)
|
||||
self._branch_label.set_text(" " + self._current_model_name)
|
||||
self._branch_label.set_position(version_pos.x + self._version_label.rect.width + self._date_label.rect.width + 20, version_pos.y)
|
||||
self._branch_label.set_position(version_pos.x + self._version_label.text_width + self._date_label.text_width + 20, version_pos.y)
|
||||
self._branch_label.render()
|
||||
|
||||
self._version_commit_label.set_text(self._version_text[2])
|
||||
self._version_commit_label.set_position(version_pos.x, version_pos.y + self._date_label.font_size + 7)
|
||||
self._version_commit_label.render()
|
||||
else:
|
||||
self._branch_label.set_max_width(gui_app.width - 32)
|
||||
self._branch_label.set_text(self._current_model_name)
|
||||
self._branch_label.set_position(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16)
|
||||
self._branch_label.render()
|
||||
if not release_branch:
|
||||
# 2nd line
|
||||
self._version_commit_label.set_text(self._version_text[2])
|
||||
self._version_commit_label.set_position(version_pos.x, version_pos.y + self._date_label.font_size + 7)
|
||||
self._version_commit_label.render()
|
||||
|
||||
self._render_bottom_status_bar()
|
||||
|
||||
def _render_bottom_status_bar(self):
|
||||
# ***** Center-aligned bottom section icons *****
|
||||
self._experimental_icon.set_visible(self._experimental_mode)
|
||||
self._mic_icon.set_visible(ui_state.recording_audio)
|
||||
|
||||
# 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):
|
||||
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)
|
||||
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
|
||||
|
||||
else:
|
||||
# No network
|
||||
# Offset by difference in height between slashless and slash icons to make center align match
|
||||
rl.draw_texture(self._wifi_slash_txt, int(last_x), int(self._rect.y + self.rect.height - self._wifi_slash_txt.height / 2 -
|
||||
(self._wifi_slash_txt.height - self._wifi_none_txt.height) / 2 - Y_CENTER),
|
||||
rl.Color(255, 255, 255, 255))
|
||||
last_x += self._wifi_slash_txt.width + ITEM_SPACING
|
||||
|
||||
# draw experimental icon
|
||||
if self._experimental_mode:
|
||||
rl.draw_texture(self._experimental_txt, int(last_x),
|
||||
int(self._rect.y + self.rect.height - self._experimental_txt.height / 2 - Y_CENTER), rl.Color(255, 255, 255, 255))
|
||||
last_x += self._experimental_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
|
||||
footer_rect = rl.Rectangle(self.rect.x + HOME_PADDING, self.rect.y + self.rect.height - 48, self.rect.width - HOME_PADDING, 48)
|
||||
self._status_bar_layout.render(footer_rect)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.system.hardware import PC
|
||||
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout
|
||||
from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts
|
||||
@@ -16,18 +14,12 @@ from openpilot.system.ui.lib.application import gui_app
|
||||
ONROAD_DELAY = 2.5 # seconds
|
||||
|
||||
|
||||
class MainState(IntEnum):
|
||||
MAIN = 0
|
||||
SETTINGS = 1
|
||||
|
||||
|
||||
class MiciMainLayout(Widget):
|
||||
class MiciMainLayout(Scroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
super().__init__(snap_items=True, spacing=0, pad=0, scroll_indicator=False, edge_shadows=False)
|
||||
|
||||
self._pm = messaging.PubMaster(['bookmarkButton'])
|
||||
|
||||
self._current_mode: MainState | None = None
|
||||
self._prev_onroad = False
|
||||
self._prev_standstill = False
|
||||
self._onroad_time_delay: float | None = None
|
||||
@@ -44,46 +36,37 @@ class MiciMainLayout(Widget):
|
||||
# TODO: set parent rect and use it if never passed rect from render (like in Scroller)
|
||||
widget.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
|
||||
self._scroller = Scroller([
|
||||
self._scroller.add_widgets([
|
||||
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._layouts = {
|
||||
MainState.MAIN: self._scroller,
|
||||
MainState.SETTINGS: self._settings_layout,
|
||||
}
|
||||
|
||||
# Set callbacks
|
||||
self._setup_callbacks()
|
||||
|
||||
# Skip onboarding on desktop; keep normal flow on device.
|
||||
self._onboarding_window = None
|
||||
if not PC:
|
||||
self._onboarding_window = OnboardingWindow()
|
||||
if not self._onboarding_window.completed:
|
||||
gui_app.set_modal_overlay(self._onboarding_window)
|
||||
gui_app.add_nav_stack_tick(self._handle_transitions)
|
||||
gui_app.push_widget(self)
|
||||
|
||||
# Start onboarding if terms or training not completed, make sure to push after self
|
||||
self._onboarding_window = OnboardingWindow(lambda: gui_app.pop_widgets_to(self))
|
||||
if not self._onboarding_window.completed:
|
||||
gui_app.push_widget(self._onboarding_window)
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self._home_layout.set_callbacks(on_settings=self._on_settings_clicked)
|
||||
self._settings_layout.set_callbacks(on_close=self._on_settings_closed)
|
||||
self._home_layout.set_callbacks(on_settings=lambda: gui_app.push_widget(self._settings_layout))
|
||||
self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
|
||||
device.add_interactive_timeout_callback(self._set_mode_for_started)
|
||||
device.add_interactive_timeout_callback(self._on_interactive_timeout)
|
||||
|
||||
def _scroll_to(self, layout: Widget):
|
||||
layout_x = int(layout.rect.x)
|
||||
self._scroller.scroll_to(layout_x, smooth=True)
|
||||
|
||||
def _render(self, _):
|
||||
# Initial show event
|
||||
if self._current_mode is None:
|
||||
self._set_mode(MainState.MAIN)
|
||||
|
||||
if not self._setup:
|
||||
if self._alerts_layout.active_alerts() > 0:
|
||||
self._scroller.scroll_to(self._alerts_layout.rect.x)
|
||||
@@ -92,59 +75,47 @@ class MiciMainLayout(Widget):
|
||||
self._setup = True
|
||||
|
||||
# Render
|
||||
if self._current_mode == MainState.MAIN:
|
||||
self._scroller.render(self._rect)
|
||||
|
||||
elif self._current_mode == MainState.SETTINGS:
|
||||
self._settings_layout.render(self._rect)
|
||||
|
||||
self._handle_transitions()
|
||||
|
||||
def _set_mode(self, mode: MainState):
|
||||
if mode != self._current_mode:
|
||||
if self._current_mode is not None:
|
||||
self._layouts[self._current_mode].hide_event()
|
||||
self._layouts[mode].show_event()
|
||||
self._current_mode = mode
|
||||
super()._render(self._rect)
|
||||
|
||||
def _handle_transitions(self):
|
||||
# Don't pop if onboarding
|
||||
if gui_app.widget_in_stack(self._onboarding_window):
|
||||
return
|
||||
|
||||
if ui_state.started != self._prev_onroad:
|
||||
self._prev_onroad = ui_state.started
|
||||
|
||||
# onroad: after delay, pop nav stack and scroll to onroad
|
||||
# offroad: immediately scroll to home, but don't pop nav stack (can stay in settings)
|
||||
if ui_state.started:
|
||||
self._onroad_time_delay = rl.get_time()
|
||||
else:
|
||||
self._set_mode_for_started(True)
|
||||
|
||||
# delay so we show home for a bit after starting
|
||||
if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY:
|
||||
self._set_mode_for_started(True)
|
||||
self._onroad_time_delay = None
|
||||
|
||||
CS = ui_state.sm["carState"]
|
||||
if not CS.standstill and self._prev_standstill:
|
||||
self._set_mode(MainState.MAIN)
|
||||
self._scroll_to(self._onroad_layout)
|
||||
self._prev_standstill = CS.standstill
|
||||
|
||||
def _set_mode_for_started(self, onroad_transition: bool = False):
|
||||
if ui_state.started:
|
||||
CS = ui_state.sm["carState"]
|
||||
# Only go onroad if car starts or is not at a standstill
|
||||
if not CS.standstill or onroad_transition:
|
||||
self._set_mode(MainState.MAIN)
|
||||
self._scroll_to(self._onroad_layout)
|
||||
else:
|
||||
# Stay in settings if car turns off while in settings
|
||||
if not onroad_transition or self._current_mode != MainState.SETTINGS:
|
||||
self._set_mode(MainState.MAIN)
|
||||
self._scroll_to(self._home_layout)
|
||||
|
||||
def _on_settings_clicked(self):
|
||||
self._set_mode(MainState.SETTINGS)
|
||||
# FIXME: these two pops can interrupt user interacting in the settings
|
||||
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
|
||||
|
||||
def _on_settings_closed(self):
|
||||
self._set_mode(MainState.MAIN)
|
||||
# When car leaves standstill, pop nav stack and scroll to onroad
|
||||
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
|
||||
|
||||
def _on_interactive_timeout(self):
|
||||
# Don't pop if onboarding
|
||||
if gui_app.widget_in_stack(self._onboarding_window):
|
||||
return
|
||||
|
||||
if ui_state.started:
|
||||
# Don't pop if at standstill
|
||||
if not ui_state.sm["carState"].standstill:
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
else:
|
||||
# Screen turns off on timeout offroad, so pop immediately without animation
|
||||
gui_app.pop_widgets_to(self, instant=True)
|
||||
self._scroll_to(self._home_layout)
|
||||
|
||||
def _on_bookmark_clicked(self):
|
||||
user_bookmark = messaging.new_message('bookmarkButton')
|
||||
|
||||
@@ -144,7 +144,7 @@ class AlertItem(Widget):
|
||||
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)
|
||||
rl.draw_texture_ex(bg_texture, rl.Vector2(self._rect.x, self._rect.y), 0.0, 1.0, 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
|
||||
@@ -183,22 +183,20 @@ class AlertItem(Widget):
|
||||
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)
|
||||
rl.draw_texture_ex(icon_texture, rl.Vector2(icon_x, icon_y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
|
||||
class MiciOffroadAlerts(Widget):
|
||||
class MiciOffroadAlerts(Scroller):
|
||||
"""Offroad alerts layout with vertical scrolling."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Create vertical scroller
|
||||
super().__init__(horizontal=False, spacing=12, pad=0)
|
||||
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,
|
||||
@@ -289,7 +287,7 @@ class MiciOffroadAlerts(Widget):
|
||||
|
||||
def show_event(self):
|
||||
"""Reset scroll position when shown and refresh alerts."""
|
||||
self._scroller.show_event()
|
||||
super().show_event()
|
||||
self._last_refresh = time.monotonic()
|
||||
self.refresh()
|
||||
|
||||
|
||||
@@ -1,35 +1,27 @@
|
||||
from enum import IntEnum
|
||||
|
||||
import weakref
|
||||
import math
|
||||
import time
|
||||
import numpy as np
|
||||
import qrcode
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import SmallButton, SmallCircleIconButton
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.slider import SmallSlider
|
||||
from openpilot.system.ui.mici_setup import TermsHeader, TermsPage as SetupTermsPage
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
|
||||
from openpilot.system.ui.widgets.button import SmallCircleIconButton
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller, Scroller
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
from openpilot.system.ui.mici_setup import GreyBigButton, BigPillButton
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.version import terms_version, training_version
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationCircleButton
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import BaseDriverCameraDialog
|
||||
|
||||
|
||||
class OnboardingState(IntEnum):
|
||||
TERMS = 0
|
||||
ONBOARDING = 1
|
||||
DECLINE = 2
|
||||
|
||||
|
||||
class DriverCameraSetupDialog(DriverCameraDialog):
|
||||
class DriverCameraSetupDialog(BaseDriverCameraDialog):
|
||||
def __init__(self):
|
||||
super().__init__(no_escape=True)
|
||||
super().__init__()
|
||||
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()
|
||||
@@ -43,7 +35,7 @@ class DriverCameraSetupDialog(DriverCameraDialog):
|
||||
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
|
||||
return
|
||||
|
||||
# Position dmoji on opposite side from driver
|
||||
is_rhd = self.driver_state_renderer.is_rhd
|
||||
@@ -56,92 +48,64 @@ class DriverCameraSetupDialog(DriverCameraDialog):
|
||||
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="continue")
|
||||
self._title_header = TermsHeader("driver monitoring setup", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
|
||||
class TrainingGuidePreDMTutorial(NavScroller):
|
||||
def __init__(self, continue_callback: Callable[[], None]):
|
||||
super().__init__()
|
||||
|
||||
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)
|
||||
continue_button = BigPillButton("next")
|
||||
continue_button.set_click_callback(continue_callback)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
GreyBigButton("driver monitoring\ncheck", "scroll to continue",
|
||||
gui_app.texture("icons_mici/setup/green_dm.png", 64, 64)),
|
||||
GreyBigButton("", "Next, we'll check if comma four can detect the driver properly."),
|
||||
GreyBigButton("", "openpilot uses the cabin camera to check if the driver is distracted."),
|
||||
GreyBigButton("", "If it does not have a clear view of the driver, unplug and remount before continuing."),
|
||||
continue_button,
|
||||
])
|
||||
|
||||
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)),
|
||||
))
|
||||
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", True)
|
||||
|
||||
|
||||
class DMBadFaceDetected(SetupTermsPage):
|
||||
def __init__(self, continue_callback, back_callback):
|
||||
super().__init__(continue_callback, back_callback, continue_text="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("Re-mount if your face is occluded or driver monitoring has difficulty tracking your face.", 42, FontWeight.ROMAN)
|
||||
class DMBadFaceDetected(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def _content_height(self):
|
||||
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
|
||||
back_button = BigPillButton("back")
|
||||
back_button.set_click_callback(self.dismiss)
|
||||
|
||||
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)),
|
||||
))
|
||||
self._scroller.add_widgets([
|
||||
GreyBigButton("looking for driver", "make sure comma\nfour can see your face",
|
||||
gui_app.texture("icons_mici/setup/orange_dm.png", 64, 64)),
|
||||
GreyBigButton("", "Remount if your face is blocked, or driver monitoring has difficulty tracking your face."),
|
||||
back_button,
|
||||
])
|
||||
|
||||
|
||||
class TrainingGuideDMTutorial(Widget):
|
||||
class TrainingGuideDMTutorial(NavWidget):
|
||||
PROGRESS_DURATION = 4
|
||||
LOOKING_THRESHOLD_DEG = 30.0
|
||||
|
||||
def __init__(self, continue_callback):
|
||||
def __init__(self, continue_callback: Callable[[], None]):
|
||||
super().__init__()
|
||||
self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 48, 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", 48, 35))
|
||||
|
||||
# Wrap the continue callback to restore settings
|
||||
def wrapped_continue_callback():
|
||||
device.set_offroad_brightness(None)
|
||||
continue_callback()
|
||||
self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 28, 48))
|
||||
self._back_button.set_click_callback(lambda: gui_app.push_widget(self._bad_face_page))
|
||||
self._back_button.set_touch_valid_callback(lambda: self.enabled and not self.is_dismissing) # for nav stack
|
||||
self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 42, 42))
|
||||
self._good_button.set_touch_valid_callback(lambda: self.enabled and not self.is_dismissing) # for nav stack
|
||||
|
||||
self._good_button.set_click_callback(wrapped_continue_callback)
|
||||
self._good_button.set_click_callback(continue_callback)
|
||||
self._good_button.set_enabled(False)
|
||||
|
||||
self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps)
|
||||
self._step_start_time = time.monotonic()
|
||||
self._dialog = DriverCameraSetupDialog()
|
||||
self._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, self._hide_bad_face_page)
|
||||
self._should_show_bad_face_page = False
|
||||
self._bad_face_page = DMBadFaceDetected()
|
||||
|
||||
# Disable driver monitoring model when device times out for inactivity
|
||||
def inactivity_callback():
|
||||
@@ -149,23 +113,10 @@ class TrainingGuideDMTutorial(Widget):
|
||||
|
||||
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._step_start_time = time.monotonic()
|
||||
|
||||
device.set_offroad_brightness(100)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
@@ -174,10 +125,6 @@ class TrainingGuideDMTutorial(Widget):
|
||||
|
||||
sm = ui_state.sm
|
||||
if sm.recv_frame.get("driverMonitoringState", 0) == 0:
|
||||
# Fallback for devices where DM model isn't publishing during onboarding:
|
||||
# allow manual continue once camera is active so setup isn't hard-blocked.
|
||||
if self._dialog._camera_view.frame and (time.monotonic() - self._step_start_time) > 2.5:
|
||||
self._good_button.set_enabled(True)
|
||||
return
|
||||
|
||||
dm_state = sm["driverMonitoringState"]
|
||||
@@ -190,7 +137,8 @@ class TrainingGuideDMTutorial(Widget):
|
||||
looking_center = False
|
||||
|
||||
# stay at 100% once reached
|
||||
if (dm_state.faceDetected and looking_center) or self._progress.x > 0.99:
|
||||
in_bad_face = gui_app.get_active_widget() == self._bad_face_page
|
||||
if ((dm_state.faceDetected and looking_center) or self._progress.x > 0.99) and not in_bad_face:
|
||||
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)
|
||||
@@ -201,13 +149,12 @@ class TrainingGuideDMTutorial(Widget):
|
||||
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)
|
||||
gradient_y = int(self._rect.y + self._rect.height - 80)
|
||||
gradient_h = int(self._rect.y) + int(self._rect.height) - gradient_y
|
||||
rl.draw_rectangle_gradient_v(int(self._rect.x), gradient_y,
|
||||
int(self._rect.width), gradient_h, rl.BLANK, rl.BLACK)
|
||||
|
||||
# draw white ring around dm icon to indicate progress
|
||||
ring_thickness = 8
|
||||
@@ -260,238 +207,181 @@ class TrainingGuideDMTutorial(Widget):
|
||||
))
|
||||
|
||||
# rounded border
|
||||
rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height))
|
||||
rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK)
|
||||
rl.end_scissor_mode()
|
||||
|
||||
|
||||
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="no", continue_text="yes")
|
||||
self._title_header = TermsHeader("improve driver monitoring", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
|
||||
|
||||
self._dm_label = UnifiedLabel("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="continue")
|
||||
self._title_header = TermsHeader("driver assistance", gui_app.texture("icons_mici/setup/warning.png", 60, 60))
|
||||
self._warning_label = UnifiedLabel("1. openpilot 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):
|
||||
class TrainingGuideRecordFront(NavScroller):
|
||||
def __init__(self, continue_callback: Callable[[], None]):
|
||||
super().__init__()
|
||||
self._completed_callback = completed_callback
|
||||
self._step = 0
|
||||
|
||||
self_ref = weakref.ref(self)
|
||||
def on_accept():
|
||||
ui_state.params.put_bool_nonblocking("RecordFront", True)
|
||||
continue_callback()
|
||||
|
||||
def on_continue():
|
||||
if obj := self_ref():
|
||||
obj._advance_step()
|
||||
def on_decline():
|
||||
ui_state.params.put_bool_nonblocking("RecordFront", False)
|
||||
continue_callback()
|
||||
|
||||
self._accept_button = BigConfirmationCircleButton("allow data uploading", gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64),
|
||||
on_accept, exit_on_confirm=False)
|
||||
|
||||
self._decline_button = BigConfirmationCircleButton("no, don't upload", gui_app.texture("icons_mici/setup/cancel.png", 64, 64), on_decline,
|
||||
exit_on_confirm=False)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
GreyBigButton("driver camera data", "do you want to share video data for training?",
|
||||
gui_app.texture("icons_mici/setup/green_dm.png", 64, 64)),
|
||||
GreyBigButton("", "Sharing your data with comma helps improve openpilot for everyone."),
|
||||
self._accept_button,
|
||||
self._decline_button,
|
||||
])
|
||||
|
||||
|
||||
class TrainingGuideAttentionNotice(Scroller):
|
||||
def __init__(self, continue_callback: Callable[[], None]):
|
||||
super().__init__()
|
||||
|
||||
continue_button = BigPillButton("next")
|
||||
continue_button.set_click_callback(continue_callback)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
GreyBigButton("what is openpilot?", "scroll to continue",
|
||||
gui_app.texture("icons_mici/setup/green_info.png", 64, 64)),
|
||||
GreyBigButton("", "1. openpilot is a driver assistance system."),
|
||||
GreyBigButton("", "2. You must pay attention at all times."),
|
||||
GreyBigButton("", "3. You must be ready to take over at any time."),
|
||||
GreyBigButton("", "4. You are fully responsible for driving the car."),
|
||||
continue_button,
|
||||
])
|
||||
|
||||
|
||||
class TrainingGuide(NavWidget):
|
||||
def __init__(self, completed_callback: Callable[[], None]):
|
||||
super().__init__()
|
||||
|
||||
self._steps = [
|
||||
TrainingGuideAttentionNotice(continue_callback=on_continue),
|
||||
TrainingGuidePreDMTutorial(continue_callback=on_continue),
|
||||
TrainingGuideDMTutorial(continue_callback=on_continue),
|
||||
TrainingGuideRecordFront(continue_callback=on_continue),
|
||||
TrainingGuideAttentionNotice(continue_callback=lambda: gui_app.push_widget(self._steps[1])),
|
||||
TrainingGuidePreDMTutorial(continue_callback=lambda: gui_app.push_widget(self._steps[2])),
|
||||
TrainingGuideDMTutorial(continue_callback=lambda: gui_app.push_widget(self._steps[3])),
|
||||
TrainingGuideRecordFront(continue_callback=completed_callback),
|
||||
]
|
||||
|
||||
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()
|
||||
self._child(self._steps[0])
|
||||
self._steps[0].set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack
|
||||
|
||||
def _render(self, _):
|
||||
if self._step < len(self._steps):
|
||||
self._steps[self._step].render(self._rect)
|
||||
return -1
|
||||
self._steps[0].render(self._rect)
|
||||
|
||||
|
||||
class DeclinePage(Widget):
|
||||
def __init__(self, back_callback=None):
|
||||
class QRCodeWidget(Widget):
|
||||
def __init__(self, url: str, size: int = 170):
|
||||
super().__init__()
|
||||
self._uninstall_slider = SmallSlider("uninstall openpilot", self._on_uninstall)
|
||||
self.set_rect(rl.Rectangle(0, 0, size, size))
|
||||
self._size = size
|
||||
self._qr_texture: rl.Texture | None = None
|
||||
self._generate_qr(url)
|
||||
|
||||
self._back_button = SmallButton("back")
|
||||
self._back_button.set_click_callback(back_callback)
|
||||
def _generate_qr(self, url: str):
|
||||
qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=0)
|
||||
qr.add_data(url)
|
||||
qr.make(fit=True)
|
||||
|
||||
self._warning_header = TermsHeader("you must accept the\nterms to use openpilot",
|
||||
gui_app.texture("icons_mici/setup/red_warning.png", 66, 60))
|
||||
pil_img = qr.make_image(fill_color="white", back_color="black").convert('RGBA')
|
||||
img_array = np.array(pil_img, dtype=np.uint8)
|
||||
|
||||
def _on_uninstall(self):
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
gui_app.request_close()
|
||||
rl_image = rl.Image()
|
||||
rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data)
|
||||
rl_image.width = pil_img.width
|
||||
rl_image.height = pil_img.height
|
||||
rl_image.mipmaps = 1
|
||||
rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
|
||||
|
||||
self._qr_texture = rl.load_texture_from_image(rl_image)
|
||||
|
||||
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,
|
||||
))
|
||||
if self._qr_texture:
|
||||
scale = self._size / self._qr_texture.height
|
||||
rl.draw_texture_ex(self._qr_texture, rl.Vector2(round(self._rect.x), round(self._rect.y)), 0.0, scale, rl.WHITE)
|
||||
|
||||
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,
|
||||
))
|
||||
def __del__(self):
|
||||
if self._qr_texture and self._qr_texture.id != 0:
|
||||
rl.unload_texture(self._qr_texture)
|
||||
|
||||
|
||||
class TermsPage(SetupTermsPage):
|
||||
def __init__(self, on_accept=None, on_decline=None):
|
||||
super().__init__(on_accept, on_decline, "decline")
|
||||
class TermsPage(Scroller):
|
||||
def __init__(self, on_accept, on_decline):
|
||||
super().__init__()
|
||||
|
||||
info_txt = gui_app.texture("icons_mici/setup/green_info.png", 60, 60)
|
||||
self._title_header = TermsHeader("terms & conditions", info_txt)
|
||||
self._accept_button = BigConfirmationCircleButton("accept\nterms", gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64), on_accept)
|
||||
self._decline_button = BigConfirmationCircleButton("decline &\nuninstall", gui_app.texture("icons_mici/setup/cancel.png", 64, 64), on_decline,
|
||||
red=True, exit_on_confirm=False)
|
||||
|
||||
self._terms_label = UnifiedLabel("You must accept the Terms and Conditions to use openpilot. " +
|
||||
"Read the latest terms at https://comma.ai/terms before continuing.", 36,
|
||||
FontWeight.ROMAN)
|
||||
self._terms_header = GreyBigButton("terms and\nconditions", "scroll to continue",
|
||||
gui_app.texture("icons_mici/setup/green_info.png", 64, 64))
|
||||
self._must_accept_card = GreyBigButton("", "You must accept the Terms & Conditions to use openpilot.")
|
||||
|
||||
@property
|
||||
def _content_height(self):
|
||||
return self._terms_label.rect.y + self._terms_label.rect.height - self._scroll_panel.get_offset()
|
||||
self._scroller.add_widgets([
|
||||
self._terms_header,
|
||||
GreyBigButton("swipe for QR code", "or go to https://comma.ai/terms",
|
||||
gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 64, 56, flip_x=True)),
|
||||
QRCodeWidget("https://comma.ai/terms"),
|
||||
self._must_accept_card,
|
||||
self._accept_button,
|
||||
self._decline_button,
|
||||
])
|
||||
|
||||
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)),
|
||||
))
|
||||
def _render(self, _):
|
||||
rl.draw_rectangle_rec(self._rect, rl.BLACK)
|
||||
super()._render(_)
|
||||
|
||||
|
||||
class OnboardingWindow(Widget):
|
||||
def __init__(self):
|
||||
def __init__(self, completed_callback: Callable[[], None]):
|
||||
super().__init__()
|
||||
self._completed_callback = completed_callback
|
||||
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))
|
||||
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
|
||||
# Windows
|
||||
self._terms = TermsPage(on_accept=self._on_terms_accepted, on_decline=self._on_terms_declined)
|
||||
self._terms = TermsPage(on_accept=self._on_terms_accepted, on_decline=self._on_uninstall)
|
||||
self._terms.set_enabled(lambda: self.enabled) # for nav stack
|
||||
self._training_guide = TrainingGuide(completed_callback=self._on_completed_training)
|
||||
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
|
||||
self._training_guide.set_enabled(lambda: self.enabled) # for nav stack
|
||||
|
||||
def _on_uninstall(self):
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
device.set_override_interactive_timeout(300)
|
||||
device.set_offroad_brightness(100)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
# FIXME: when nav stack sends hide event to widget 2 below on push, this needs to be moved
|
||||
device.set_override_interactive_timeout(None)
|
||||
device.set_offroad_brightness(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)
|
||||
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", False)
|
||||
self._completed_callback()
|
||||
|
||||
def _on_terms_accepted(self):
|
||||
ui_state.params.put("HasAcceptedTerms", terms_version)
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
gui_app.push_widget(self._training_guide)
|
||||
|
||||
def _on_completed_training(self):
|
||||
ui_state.params.put("CompletedTrainingVersion", training_version)
|
||||
self.close()
|
||||
|
||||
def _render(self, _):
|
||||
if self._state == OnboardingState.TERMS:
|
||||
self._terms.render(self._rect)
|
||||
elif self._state == OnboardingState.ONBOARDING:
|
||||
self._training_guide.render(self._rect)
|
||||
elif self._state == OnboardingState.DECLINE:
|
||||
self._decline_page.render(self._rect)
|
||||
return -1
|
||||
rl.draw_rectangle_rec(self._rect, rl.BLACK)
|
||||
self._terms.render(self._rect)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle, BigParamControl
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle, BigParamControl, BigCircleParamControl
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigInputDialog, BigMultiOptionDialog
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.fingerprint_catalog import (
|
||||
FingerprintModelOption,
|
||||
@@ -12,106 +9,119 @@ from openpilot.selfdrive.ui.mici.layouts.settings.fingerprint_catalog import (
|
||||
shorten_model_label,
|
||||
)
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import NavWidget
|
||||
from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyAction
|
||||
from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyFetcher
|
||||
|
||||
|
||||
class DeveloperLayoutMici(NavWidget):
|
||||
def __init__(self, back_callback: Callable):
|
||||
class DeveloperLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_back_callback(back_callback)
|
||||
self._ssh_fetcher = SshKeyFetcher(ui_state.params)
|
||||
self._make_options, self._models_by_make, self._models_by_value, self._make_by_model = get_fingerprint_catalog()
|
||||
|
||||
def github_username_callback(username: str):
|
||||
if username:
|
||||
ssh_keys = SshKeyAction()
|
||||
ssh_keys._fetch_ssh_key(username)
|
||||
if not ssh_keys._error_message:
|
||||
self._ssh_keys_btn.set_value(username)
|
||||
else:
|
||||
dlg = BigDialog("", ssh_keys._error_message)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
self._ssh_keys_btn.set_value("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("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("Not set")
|
||||
|
||||
def ssh_keys_callback():
|
||||
github_username = ui_state.params.get("GithubUsername") or ""
|
||||
dlg = BigInputDialog("enter GitHub username", github_username, confirm_callback=github_username_callback)
|
||||
dlg = BigInputDialog("enter GitHub username...", github_username, minimum_length=0, confirm_callback=github_username_callback)
|
||||
if not system_time_valid():
|
||||
dlg = BigDialog("Please connect to Wi-Fi to fetch your key", "")
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
dlg = BigDialog("", "Please connect to Wi-Fi to fetch your key.")
|
||||
gui_app.push_widget(dlg)
|
||||
return
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
txt_ssh = gui_app.texture("icons_mici/settings/developer/ssh.png", 77, 44)
|
||||
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("SSH keys", "Not set" if not github_username else github_username, icon=txt_ssh)
|
||||
self._ssh_keys_btn.set_click_callback(ssh_keys_callback)
|
||||
|
||||
# Fingerprint controls
|
||||
(
|
||||
self._make_options,
|
||||
self._models_by_make,
|
||||
self._models_by_value,
|
||||
self._make_by_model,
|
||||
) = get_fingerprint_catalog()
|
||||
self._car_make_btn = BigButton("car make", self._get_display_make())
|
||||
self._car_make_btn.set_click_callback(self._open_make_selector)
|
||||
self._car_model_btn = BigButton("car model", self._get_display_model())
|
||||
self._car_model_btn.set_click_callback(self._open_model_selector)
|
||||
self._force_fingerprint_toggle = BigParamControl(
|
||||
"disable auto fingerprint", "ForceFingerprint", toggle_callback=lambda checked: restart_needed_callback(checked)
|
||||
"disable auto fingerprint", "ForceFingerprint", toggle_callback=restart_needed_callback,
|
||||
)
|
||||
|
||||
# adb, ssh, ssh keys, debug mode, joystick debug mode, longitudinal maneuver mode, ip address
|
||||
# ******** Main Scroller ********
|
||||
self._adb_toggle = BigParamControl("enable ADB", "AdbEnabled")
|
||||
self._adb_toggle = BigCircleParamControl(gui_app.texture("icons_mici/adb_short.png", 82, 82), "AdbEnabled", icon_offset=(0, 12))
|
||||
self._ssh_toggle = BigCircleParamControl(gui_app.texture("icons_mici/ssh_short.png", 82, 82), "SshEnabled", icon_offset=(0, 12))
|
||||
self._use_prebuilt_toggle = BigParamControl("use prebuilt binaries", "UsePrebuilt")
|
||||
self._ssh_toggle = BigParamControl("enable SSH", "SshEnabled")
|
||||
self._joystick_toggle = BigToggle(
|
||||
"joystick debug mode", initial_state=ui_state.params.get_bool("JoystickDebugMode"), toggle_callback=self._on_joystick_debug_mode
|
||||
)
|
||||
self._long_maneuver_toggle = BigToggle(
|
||||
"longitudinal maneuver mode", initial_state=ui_state.params.get_bool("LongitudinalManeuverMode"), toggle_callback=self._on_long_maneuver_mode
|
||||
)
|
||||
self._alpha_long_toggle = BigToggle(
|
||||
"alpha longitudinal", initial_state=ui_state.params.get_bool("AlphaLongitudinalEnabled"), toggle_callback=self._on_alpha_long_enabled
|
||||
)
|
||||
self._debug_mode_toggle = BigParamControl(
|
||||
"ui debug mode", "ShowDebugInfo", toggle_callback=lambda checked: (gui_app.set_show_touches(checked), gui_app.set_show_fps(checked))
|
||||
)
|
||||
self._joystick_toggle = BigToggle("joystick debug mode",
|
||||
initial_state=ui_state.params.get_bool("JoystickDebugMode"),
|
||||
toggle_callback=self._on_joystick_debug_mode)
|
||||
self._long_maneuver_toggle = BigToggle("longitudinal maneuver mode",
|
||||
initial_state=ui_state.params.get_bool("LongitudinalManeuverMode"),
|
||||
toggle_callback=self._on_long_maneuver_mode)
|
||||
self._lat_maneuver_toggle = BigToggle("lateral maneuver mode",
|
||||
initial_state=ui_state.params.get_bool("LateralManeuverMode"),
|
||||
toggle_callback=self._on_lat_maneuver_mode)
|
||||
self._alpha_long_toggle = BigToggle("alpha longitudinal",
|
||||
initial_state=ui_state.params.get_bool("AlphaLongitudinalEnabled"),
|
||||
toggle_callback=self._on_alpha_long_enabled)
|
||||
self._debug_mode_toggle = BigParamControl("ui debug mode", "ShowDebugInfo",
|
||||
toggle_callback=lambda checked: (gui_app.set_show_touches(checked),
|
||||
gui_app.set_show_fps(checked)))
|
||||
|
||||
self._scroller = Scroller(
|
||||
[
|
||||
self._adb_toggle,
|
||||
self._use_prebuilt_toggle,
|
||||
self._ssh_toggle,
|
||||
self._ssh_keys_btn,
|
||||
self._car_make_btn,
|
||||
self._car_model_btn,
|
||||
self._force_fingerprint_toggle,
|
||||
self._joystick_toggle,
|
||||
self._long_maneuver_toggle,
|
||||
self._alpha_long_toggle,
|
||||
self._debug_mode_toggle,
|
||||
],
|
||||
snap_items=False,
|
||||
scroll_indicator=True,
|
||||
edge_shadows=True,
|
||||
)
|
||||
self._scroller.add_widgets([
|
||||
self._adb_toggle,
|
||||
self._ssh_toggle,
|
||||
self._ssh_keys_btn,
|
||||
self._car_make_btn,
|
||||
self._car_model_btn,
|
||||
self._force_fingerprint_toggle,
|
||||
self._use_prebuilt_toggle,
|
||||
self._joystick_toggle,
|
||||
self._long_maneuver_toggle,
|
||||
self._lat_maneuver_toggle,
|
||||
self._alpha_long_toggle,
|
||||
self._debug_mode_toggle,
|
||||
])
|
||||
|
||||
# Toggle lists
|
||||
self._refresh_toggles = (
|
||||
("AdbEnabled", self._adb_toggle),
|
||||
("UsePrebuilt", self._use_prebuilt_toggle),
|
||||
("SshEnabled", self._ssh_toggle),
|
||||
("ForceFingerprint", self._force_fingerprint_toggle),
|
||||
("UsePrebuilt", self._use_prebuilt_toggle),
|
||||
("JoystickDebugMode", self._joystick_toggle),
|
||||
("LongitudinalManeuverMode", self._long_maneuver_toggle),
|
||||
("LateralManeuverMode", self._lat_maneuver_toggle),
|
||||
("AlphaLongitudinalEnabled", self._alpha_long_toggle),
|
||||
("ShowDebugInfo", self._debug_mode_toggle),
|
||||
)
|
||||
onroad_blocked_toggles = (self._adb_toggle, self._car_make_btn, self._car_model_btn, self._force_fingerprint_toggle, self._joystick_toggle)
|
||||
engaged_blocked_toggles = (self._long_maneuver_toggle, self._alpha_long_toggle)
|
||||
onroad_blocked_toggles = (
|
||||
self._adb_toggle,
|
||||
self._car_make_btn,
|
||||
self._car_model_btn,
|
||||
self._force_fingerprint_toggle,
|
||||
self._use_prebuilt_toggle,
|
||||
self._joystick_toggle,
|
||||
)
|
||||
release_blocked_toggles = (self._joystick_toggle, self._long_maneuver_toggle, self._lat_maneuver_toggle, self._alpha_long_toggle)
|
||||
engaged_blocked_toggles = (self._long_maneuver_toggle, self._lat_maneuver_toggle, self._alpha_long_toggle)
|
||||
|
||||
# Hide non-release toggles on release builds
|
||||
for item in release_blocked_toggles:
|
||||
item.set_visible(not ui_state.is_release)
|
||||
|
||||
# Disable toggles that require offroad
|
||||
for item in onroad_blocked_toggles:
|
||||
@@ -128,23 +138,43 @@ class DeveloperLayoutMici(NavWidget):
|
||||
|
||||
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._car_make_btn.set_value(self._get_display_make())
|
||||
self._car_model_btn.set_value(self._get_display_model())
|
||||
self._update_toggles()
|
||||
|
||||
def _show_option_dialog(self, title: str, options: list[str], current: str, on_selected):
|
||||
dialog_holder: dict[str, BigMultiOptionDialog] = {}
|
||||
|
||||
def on_confirm():
|
||||
on_selected(dialog_holder["dialog"].get_selected_option())
|
||||
|
||||
dialog = BigMultiOptionDialog(options=options, default=current, right_btn_callback=on_confirm)
|
||||
dialog_holder["dialog"] = dialog
|
||||
gui_app.push_widget(dialog)
|
||||
|
||||
def _get_display_make(self) -> str:
|
||||
make = ui_state.params.get("CarMake") or ""
|
||||
make = ui_state.params.get("CarMake", encoding="utf-8") or ""
|
||||
if make:
|
||||
return make
|
||||
|
||||
model = ui_state.params.get("CarModel") or ""
|
||||
model = ui_state.params.get("CarModel", encoding="utf-8") or ""
|
||||
if model:
|
||||
return self._make_by_model.get(model, format_fingerprint_value(model.split("_", 1)[0]))
|
||||
return "Select"
|
||||
|
||||
def _get_selected_model_option(self) -> FingerprintModelOption | None:
|
||||
model = ui_state.params.get("CarModel") or ""
|
||||
model = ui_state.params.get("CarModel", encoding="utf-8") or ""
|
||||
if not model:
|
||||
return None
|
||||
|
||||
model_name = ui_state.params.get("CarModelName") or ""
|
||||
make = ui_state.params.get("CarMake") or self._make_by_model.get(model, "")
|
||||
model_name = ui_state.params.get("CarModelName", encoding="utf-8") or ""
|
||||
make = ui_state.params.get("CarMake", encoding="utf-8") or self._make_by_model.get(model, "")
|
||||
if make and model_name:
|
||||
for option in self._models_by_make.get(make, ()):
|
||||
if option.value == model and option.label == model_name:
|
||||
@@ -157,8 +187,8 @@ class DeveloperLayoutMici(NavWidget):
|
||||
if selected_option is not None:
|
||||
return selected_option.button_label
|
||||
|
||||
model = ui_state.params.get("CarModel") or ""
|
||||
model_name = ui_state.params.get("CarModelName") or ""
|
||||
model = ui_state.params.get("CarModel", encoding="utf-8") or ""
|
||||
model_name = ui_state.params.get("CarModelName", encoding="utf-8") or ""
|
||||
if model:
|
||||
model_option = self._models_by_value.get(model)
|
||||
if model_option is not None:
|
||||
@@ -197,54 +227,40 @@ class DeveloperLayoutMici(NavWidget):
|
||||
def _open_make_selector(self):
|
||||
options = list(self._make_options)
|
||||
if not options:
|
||||
gui_app.set_modal_overlay(BigDialog("No fingerprint list available", ""))
|
||||
gui_app.push_widget(BigDialog("", "No fingerprint list available"))
|
||||
return
|
||||
|
||||
current_make = self._get_display_make()
|
||||
default_make = current_make if current_make in options else options[0]
|
||||
|
||||
def on_selected():
|
||||
selected_make = option_dialog.get_selected_option()
|
||||
def on_selected(selected_make: str):
|
||||
self._set_car_make(selected_make)
|
||||
|
||||
current_model = ui_state.params.get("CarModel") or ""
|
||||
current_model = ui_state.params.get("CarModel", encoding="utf-8") or ""
|
||||
available_models = {option.value for option in self._models_by_make.get(selected_make, ())}
|
||||
if current_model not in available_models:
|
||||
default_model = self._models_by_make[selected_make][0]
|
||||
self._set_car_model(default_model)
|
||||
if current_model not in available_models and self._models_by_make.get(selected_make):
|
||||
self._set_car_model(self._models_by_make[selected_make][0])
|
||||
|
||||
option_dialog = BigMultiOptionDialog(options=options, default=default_make, right_btn_callback=on_selected)
|
||||
gui_app.set_modal_overlay(option_dialog)
|
||||
self._show_option_dialog("select make", options, default_make, on_selected)
|
||||
|
||||
def _open_model_selector(self):
|
||||
make = self._get_display_make()
|
||||
model_options = self._models_by_make.get(make, ())
|
||||
if not model_options:
|
||||
gui_app.set_modal_overlay(BigDialog("Select a car make first", ""))
|
||||
gui_app.push_widget(BigDialog("", "Select a car make first"))
|
||||
return
|
||||
|
||||
current_model = ui_state.params.get("CarModel") or ""
|
||||
current_model_name = ui_state.params.get("CarModelName") or ""
|
||||
current_model = ui_state.params.get("CarModel", encoding="utf-8") or ""
|
||||
current_model_name = ui_state.params.get("CarModelName", encoding="utf-8") or ""
|
||||
option_labels = [option.option_label for option in model_options]
|
||||
selected_by_label = {option.option_label: option for option in model_options}
|
||||
default_model = next((option.option_label for option in model_options if option.value == current_model and option.label == current_model_name), None)
|
||||
if default_model is None:
|
||||
default_model = next((option.option_label for option in model_options if option.value == current_model), option_labels[0])
|
||||
|
||||
def on_selected():
|
||||
selected_model = selected_by_label[option_dialog.get_selected_option()]
|
||||
self._set_car_model(selected_model)
|
||||
def on_selected(selected_label: str):
|
||||
self._set_car_model(selected_by_label[selected_label])
|
||||
|
||||
option_dialog = BigMultiOptionDialog(options=option_labels, default=default_model, right_btn_callback=on_selected)
|
||||
gui_app.set_modal_overlay(option_dialog)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
self._update_toggles()
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._scroller.render(rect)
|
||||
self._show_option_dialog("select model", option_labels, default_model, on_selected)
|
||||
|
||||
def _update_toggles(self):
|
||||
ui_state.update_params()
|
||||
@@ -252,7 +268,7 @@ class DeveloperLayoutMici(NavWidget):
|
||||
# CP gating
|
||||
if ui_state.CP is not None:
|
||||
alpha_avail = ui_state.CP.alphaLongitudinalAvailable
|
||||
if not alpha_avail:
|
||||
if not alpha_avail or ui_state.is_release:
|
||||
self._alpha_long_toggle.set_visible(False)
|
||||
ui_state.params.remove("AlphaLongitudinalEnabled")
|
||||
else:
|
||||
@@ -263,8 +279,12 @@ class DeveloperLayoutMici(NavWidget):
|
||||
if not long_man_enabled:
|
||||
self._long_maneuver_toggle.set_checked(False)
|
||||
ui_state.params.put_bool("LongitudinalManeuverMode", False)
|
||||
|
||||
lat_man_enabled = ui_state.is_offroad()
|
||||
self._lat_maneuver_toggle.set_enabled(lat_man_enabled)
|
||||
else:
|
||||
self._long_maneuver_toggle.set_enabled(False)
|
||||
self._lat_maneuver_toggle.set_enabled(False)
|
||||
self._alpha_long_toggle.set_visible(False)
|
||||
|
||||
# Refresh toggles from params to mirror external changes
|
||||
@@ -278,11 +298,24 @@ class DeveloperLayoutMici(NavWidget):
|
||||
ui_state.params.put_bool("JoystickDebugMode", state)
|
||||
ui_state.params.put_bool("LongitudinalManeuverMode", False)
|
||||
self._long_maneuver_toggle.set_checked(False)
|
||||
ui_state.params.put_bool("LateralManeuverMode", False)
|
||||
self._lat_maneuver_toggle.set_checked(False)
|
||||
|
||||
def _on_long_maneuver_mode(self, state: bool):
|
||||
ui_state.params.put_bool("LongitudinalManeuverMode", state)
|
||||
ui_state.params.put_bool("JoystickDebugMode", False)
|
||||
self._joystick_toggle.set_checked(False)
|
||||
ui_state.params.put_bool("LateralManeuverMode", False)
|
||||
self._lat_maneuver_toggle.set_checked(False)
|
||||
restart_needed_callback(state)
|
||||
|
||||
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("JoystickDebugMode", False)
|
||||
self._joystick_toggle.set_checked(False)
|
||||
ui_state.params.put_bool("LongitudinalManeuverMode", False)
|
||||
self._long_maneuver_toggle.set_checked(False)
|
||||
restart_needed_callback(state)
|
||||
|
||||
def _on_alpha_long_enabled(self, state: bool):
|
||||
|
||||
@@ -1,47 +1,52 @@
|
||||
import os
|
||||
import threading
|
||||
import json
|
||||
import hashlib
|
||||
import secrets
|
||||
import string
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton, BigParamControl
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigDialog, BigConfirmationDialogV2, BigInputDialog
|
||||
from openpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
|
||||
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide
|
||||
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget, NavWidget
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||
from openpilot.system.ui.widgets.label import MiciLabel
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer
|
||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||
from openpilot.system.hardware import PC
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
|
||||
import qrcode
|
||||
import numpy as np
|
||||
|
||||
|
||||
class MiciFccModal(NavWidget):
|
||||
BACK_TOUCH_AREA_PERCENTAGE = 0.1
|
||||
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._terms_header.set_visible(False)
|
||||
self._must_accept_card.set_visible(False)
|
||||
self._accept_button.set_visible(False)
|
||||
self._decline_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_nonblocking("IsDriverViewEnabled", False)
|
||||
|
||||
|
||||
class MiciFccModal(NavRawScrollPanel):
|
||||
def __init__(self, file_path: str | None = None, text: str | None = None):
|
||||
super().__init__()
|
||||
self.set_back_callback(lambda: gui_app.set_modal_overlay(None))
|
||||
self._content = HtmlRenderer(file_path=file_path, text=text)
|
||||
self._scroll_panel = GuiScrollPanel2(horizontal=False)
|
||||
self._fcc_logo = gui_app.texture("icons_mici/settings/device/fcc_logo.png", 76, 64)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
@@ -58,39 +63,32 @@ class MiciFccModal(NavWidget):
|
||||
|
||||
rl.draw_texture_ex(self._fcc_logo, fcc_pos, 0.0, 1.0, rl.WHITE)
|
||||
|
||||
return -1
|
||||
|
||||
|
||||
def _engaged_confirmation_callback(callback: Callable, action_text: str):
|
||||
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()
|
||||
|
||||
red = False
|
||||
if action_text == "power off":
|
||||
icon = "icons_mici/settings/device/power.png"
|
||||
red = True
|
||||
elif action_text == "reboot":
|
||||
icon = "icons_mici/settings/device/reboot.png"
|
||||
elif action_text == "reset":
|
||||
icon = "icons_mici/settings/device/lkas.png"
|
||||
elif action_text == "reset driver monitoring":
|
||||
icon = "icons_mici/settings/device/cameras.png"
|
||||
elif action_text == "uninstall":
|
||||
icon = "icons_mici/settings/device/uninstall.png"
|
||||
else:
|
||||
# TODO: check
|
||||
icon = "icons_mici/settings/comma_icon.png"
|
||||
|
||||
dlg: BigConfirmationDialogV2 | BigDialog = BigConfirmationDialogV2(f"slide to\n{action_text.lower()}", icon, red=red,
|
||||
exit_on_confirm=action_text in {"reset", "reset driver monitoring"},
|
||||
confirm_callback=confirm_callback)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
gui_app.push_widget(BigConfirmationDialog(f"slide to\n{action_text.lower()}", icon, confirm_callback, exit_on_confirm=exit_on_confirm, red=red))
|
||||
else:
|
||||
dlg = BigDialog(f"Disengage to {action_text}", "")
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
gui_app.push_widget(BigDialog("", f"Disengage to {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):
|
||||
@@ -100,14 +98,15 @@ class DeviceInfoLayoutMici(Widget):
|
||||
self.set_rect(rl.Rectangle(0, 0, 360, 180))
|
||||
|
||||
params = Params()
|
||||
header_color = rl.Color(255, 255, 255, int(255 * 0.9))
|
||||
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
|
||||
max_width = int(self._rect.width - 20)
|
||||
self._dongle_id_label = MiciLabel("device ID", 48, width=max_width, color=header_color, font_weight=FontWeight.DISPLAY)
|
||||
self._dongle_id_text_label = MiciLabel(params.get("DongleId") or 'N/A', 32, width=max_width, color=subheader_color, font_weight=FontWeight.ROMAN)
|
||||
self._dongle_id_label = UnifiedLabel("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 = MiciLabel("serial", 48, color=header_color, font_weight=FontWeight.DISPLAY)
|
||||
self._serial_number_text_label = MiciLabel(params.get("HardwareSerial") or 'N/A', 32, width=max_width, color=subheader_color, font_weight=FontWeight.ROMAN)
|
||||
self._serial_number_label = UnifiedLabel("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)
|
||||
@@ -131,9 +130,14 @@ class UpdaterState(IntEnum):
|
||||
|
||||
class PairBigButton(BigButton):
|
||||
def __init__(self):
|
||||
super().__init__("pair", "connect.comma.ai", "icons_mici/settings/comma_icon.png")
|
||||
super().__init__("pair", "connect.comma.ai", gui_app.texture("icons_mici/settings/comma_icon.png", 33, 60))
|
||||
|
||||
def _get_label_font_size(self):
|
||||
return 64
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
if ui_state.prime_state.is_paired():
|
||||
self.set_text("paired")
|
||||
if ui_state.prime_state.is_prime():
|
||||
@@ -152,191 +156,27 @@ class PairBigButton(BigButton):
|
||||
return
|
||||
dlg: BigDialog | PairingDialog
|
||||
if not system_time_valid():
|
||||
dlg = BigDialog(tr("Please connect to Wi-Fi to complete initial pairing"), "")
|
||||
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 the comma.ai backend to pair"), "")
|
||||
dlg = BigDialog("", tr("Device must be registered with the comma.ai backend to pair."))
|
||||
else:
|
||||
dlg = PairingDialog()
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
|
||||
class GalaxyQRDialog(NavWidget):
|
||||
def __init__(self, url: str):
|
||||
super().__init__()
|
||||
self._url = url
|
||||
self._qr_texture: rl.Texture | None = None
|
||||
|
||||
self.set_back_callback(lambda: gui_app.set_modal_overlay(None))
|
||||
|
||||
self._title = MiciLabel("pair with galaxy", 48, font_weight=FontWeight.BOLD,
|
||||
color=rl.Color(255, 255, 255, int(255 * 0.9)), line_height=40, wrap_text=True)
|
||||
self._generate_qr_code()
|
||||
|
||||
def _generate_qr_code(self) -> None:
|
||||
try:
|
||||
qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=0)
|
||||
qr.add_data(self._url)
|
||||
qr.make(fit=True)
|
||||
|
||||
pil_img = qr.make_image(fill_color="white", back_color="black").convert("RGBA")
|
||||
img_array = np.array(pil_img, dtype=np.uint8)
|
||||
|
||||
if self._qr_texture and self._qr_texture.id != 0:
|
||||
rl.unload_texture(self._qr_texture)
|
||||
|
||||
rl_image = rl.Image()
|
||||
rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data)
|
||||
rl_image.width = pil_img.width
|
||||
rl_image.height = pil_img.height
|
||||
rl_image.mipmaps = 1
|
||||
rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
|
||||
self._qr_texture = rl.load_texture_from_image(rl_image)
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"Galaxy QR generation failed: {e}")
|
||||
self._qr_texture = None
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if self._qr_texture is not None:
|
||||
scale = rect.height / self._qr_texture.height
|
||||
pos = rl.Vector2(rect.x + 8, rect.y)
|
||||
rl.draw_texture_ex(self._qr_texture, pos, 0.0, scale, rl.WHITE)
|
||||
else:
|
||||
rl.draw_text_ex(
|
||||
gui_app.font(FontWeight.BOLD),
|
||||
"QR Code Error",
|
||||
rl.Vector2(rect.x + 20, rect.y + rect.height // 2 - 15),
|
||||
30,
|
||||
0.0,
|
||||
rl.RED,
|
||||
)
|
||||
|
||||
label_x = rect.x + 8 + rect.height + 24
|
||||
self._title.set_width(int(rect.width - label_x))
|
||||
self._title.set_position(label_x, rect.y + 28)
|
||||
self._title.render()
|
||||
|
||||
return -1
|
||||
|
||||
def __del__(self):
|
||||
if self._qr_texture and self._qr_texture.id != 0:
|
||||
rl.unload_texture(self._qr_texture)
|
||||
|
||||
|
||||
class GalaxyBigButton(BigButton):
|
||||
_SLUG_CHARS = string.ascii_letters + string.digits
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("galaxy", "", gui_app.starpilot_texture("../system/the_pond/assets/images/main_logo.png", 64, 64))
|
||||
self._galaxy_dir = Path(Paths.comma_home()) / "starpilot" / "data" / "galaxy" if PC else Path("/data/galaxy")
|
||||
self._auth_path = self._galaxy_dir / "glxyauth"
|
||||
self._session_path = self._galaxy_dir / "glxysession"
|
||||
self._slug_path = self._galaxy_dir / "glxyslug"
|
||||
|
||||
def _is_paired(self) -> bool:
|
||||
try:
|
||||
return len(self._auth_path.read_text(encoding="utf-8").strip()) == 64
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _get_slug(self) -> str:
|
||||
try:
|
||||
return self._slug_path.read_text(encoding="utf-8").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _show_qr(self):
|
||||
slug = self._get_slug()
|
||||
if not slug:
|
||||
gui_app.set_modal_overlay(BigDialog("Galaxy is not paired yet.", ""))
|
||||
return
|
||||
gui_app.set_modal_overlay(GalaxyQRDialog(f"https://galaxy.firestar.link/{slug}"))
|
||||
|
||||
def _pair_with_pin(self, pin: str):
|
||||
clean_pin = str(pin or "").strip()
|
||||
if len(clean_pin) < 6:
|
||||
gui_app.set_modal_overlay(BigDialog("PIN must be at least 6 characters.", ""))
|
||||
return
|
||||
|
||||
try:
|
||||
self._galaxy_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._auth_path.write_text(hashlib.sha256(clean_pin.encode("utf-8")).hexdigest(), encoding="utf-8")
|
||||
self._session_path.write_text(secrets.token_hex(32), encoding="utf-8")
|
||||
slug = "".join(secrets.choice(self._SLUG_CHARS) for _ in range(16))
|
||||
self._slug_path.write_text(slug, encoding="utf-8")
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"Galaxy pairing write failed: {e}")
|
||||
gui_app.set_modal_overlay(BigDialog("Failed to pair with Galaxy.", ""))
|
||||
return
|
||||
|
||||
self._show_qr()
|
||||
|
||||
def _unpair(self):
|
||||
for path in (self._auth_path, self._session_path, self._slug_path):
|
||||
try:
|
||||
path.unlink(missing_ok=True)
|
||||
except TypeError:
|
||||
# Python < 3.8 fallback
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"Galaxy unpair cleanup failed for {path}: {e}")
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
if self._is_paired():
|
||||
show_qr_option = "show qr"
|
||||
unpair_option = "unpair"
|
||||
|
||||
def on_option_selected():
|
||||
selected = option_dialog.get_selected_option()
|
||||
if selected == show_qr_option:
|
||||
self._show_qr()
|
||||
elif selected == unpair_option:
|
||||
confirm = BigConfirmationDialogV2(
|
||||
"slide to\nunpair galaxy",
|
||||
"icons_mici/settings/device/uninstall.png",
|
||||
red=True,
|
||||
confirm_callback=self._unpair,
|
||||
)
|
||||
gui_app.set_modal_overlay(confirm)
|
||||
|
||||
option_dialog = BigMultiOptionDialog(
|
||||
options=[show_qr_option, unpair_option],
|
||||
default=show_qr_option,
|
||||
right_btn_callback=on_option_selected,
|
||||
)
|
||||
gui_app.set_modal_overlay(option_dialog)
|
||||
return
|
||||
|
||||
pin_dialog = BigInputDialog(
|
||||
hint="enter galaxy pin...",
|
||||
default_text="",
|
||||
minimum_length=6,
|
||||
confirm_callback=self._pair_with_pin,
|
||||
)
|
||||
gui_app.set_modal_overlay(pin_dialog)
|
||||
|
||||
def _update_state(self):
|
||||
self.set_value("paired" if self._is_paired() else "pair")
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
|
||||
UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond
|
||||
UPDATE_SCREEN_TIMEOUT = 180 # Keep display awake for 3 minutes during long-running update phases.
|
||||
EXTENDED_TIMEOUT_UPDATER_STATES = {"downloading...", "finalizing update..."}
|
||||
|
||||
|
||||
class UpdateOpenpilotBigButton(BigButton):
|
||||
def __init__(self):
|
||||
self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 64)
|
||||
self._txt_reboot_icon = gui_app.texture("icons_mici/settings/device/reboot.png", 64, 64)
|
||||
self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75)
|
||||
self._txt_reboot_icon = gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70)
|
||||
self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
|
||||
super().__init__("update openpilot", "", self._txt_update_icon)
|
||||
|
||||
self._waiting_for_updater_t: float | None = None
|
||||
self._hide_value_t: float | None = None
|
||||
self._state: UpdaterState = UpdaterState.IDLE
|
||||
self._extended_timeout_enabled = False
|
||||
|
||||
ui_state.add_offroad_transition_callback(self.offroad_transition)
|
||||
|
||||
@@ -345,9 +185,11 @@ class UpdateOpenpilotBigButton(BigButton):
|
||||
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.set_modal_overlay(dlg)
|
||||
dlg = BigDialog("", tr("Please connect to Wi-Fi to update."))
|
||||
gui_app.push_widget(dlg)
|
||||
return
|
||||
|
||||
self.set_enabled(False)
|
||||
@@ -364,17 +206,6 @@ class UpdateOpenpilotBigButton(BigButton):
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._set_extended_timeout(False)
|
||||
|
||||
def _set_extended_timeout(self, enabled: bool):
|
||||
if self._extended_timeout_enabled == enabled:
|
||||
return
|
||||
|
||||
self._extended_timeout_enabled = enabled
|
||||
device.set_override_interactive_timeout(UPDATE_SCREEN_TIMEOUT if enabled else None)
|
||||
|
||||
def set_value(self, value: str):
|
||||
super().set_value(value)
|
||||
if value:
|
||||
@@ -383,13 +214,13 @@ class UpdateOpenpilotBigButton(BigButton):
|
||||
self.set_text("update openpilot")
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
if ui_state.started:
|
||||
self._set_extended_timeout(False)
|
||||
self.set_enabled(False)
|
||||
return
|
||||
|
||||
updater_state = ui_state.params.get("UpdaterState") or ""
|
||||
should_extend_timeout = updater_state in EXTENDED_TIMEOUT_UPDATER_STATES
|
||||
failed_count = ui_state.params.get("UpdateFailedCount")
|
||||
failed = False if failed_count is None else int(failed_count) > 0
|
||||
|
||||
@@ -411,7 +242,7 @@ class UpdateOpenpilotBigButton(BigButton):
|
||||
|
||||
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("updater failed to respond")
|
||||
self.set_value("updater failed\nto respond")
|
||||
self._state = UpdaterState.IDLE
|
||||
self._hide_value_t = rl.get_time()
|
||||
|
||||
@@ -453,16 +284,12 @@ class UpdateOpenpilotBigButton(BigButton):
|
||||
if self._state != UpdaterState.WAITING_FOR_UPDATER:
|
||||
self._waiting_for_updater_t = None
|
||||
|
||||
self._set_extended_timeout(should_extend_timeout)
|
||||
|
||||
|
||||
class DeviceLayoutMici(NavWidget):
|
||||
def __init__(self, back_callback: Callable):
|
||||
class DeviceLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._fcc_dialog: HtmlModal | None = None
|
||||
self._driver_camera: DriverCameraDialog | None = None
|
||||
self._training_guide: TrainingGuide | None = None
|
||||
|
||||
def power_off_callback():
|
||||
ui_state.params.put_bool("DoShutdown", True)
|
||||
@@ -479,106 +306,52 @@ class DeviceLayoutMici(NavWidget):
|
||||
params.remove("LiveDelay")
|
||||
params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def reset_driver_monitoring_callback():
|
||||
params = ui_state.params
|
||||
params.remove("IsRhdDetected")
|
||||
params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def uninstall_openpilot_callback():
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
|
||||
reset_driver_monitoring_btn = BigButton("reset driver monitoring calibration", "", "icons_mici/settings/device/cameras.png")
|
||||
reset_driver_monitoring_btn.set_click_callback(lambda: _engaged_confirmation_callback(reset_driver_monitoring_callback, "reset driver monitoring"))
|
||||
reset_calibration_btn = EngagedConfirmationButton("reset calibration", "reset", gui_app.texture("icons_mici/settings/device/lkas.png", 122, 64),
|
||||
reset_calibration_callback)
|
||||
|
||||
reset_calibration_btn = BigButton("reset calibration", "", "icons_mici/settings/device/lkas.png")
|
||||
reset_calibration_btn.set_click_callback(lambda: _engaged_confirmation_callback(reset_calibration_callback, "reset"))
|
||||
uninstall_openpilot_btn = EngagedConfirmationButton("uninstall openpilot", "uninstall",
|
||||
gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64),
|
||||
uninstall_openpilot_callback, exit_on_confirm=False)
|
||||
|
||||
uninstall_openpilot_btn = BigButton("uninstall openpilot", "", "icons_mici/settings/device/uninstall.png")
|
||||
uninstall_openpilot_btn.set_click_callback(lambda: _engaged_confirmation_callback(uninstall_openpilot_callback, "uninstall"))
|
||||
reboot_btn = EngagedConfirmationCircleButton("reboot", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70),
|
||||
reboot_callback, exit_on_confirm=False)
|
||||
|
||||
reboot_btn = BigCircleButton("icons_mici/settings/device/reboot.png", red=False)
|
||||
reboot_btn.set_click_callback(lambda: _engaged_confirmation_callback(reboot_callback, "reboot"))
|
||||
self._power_off_btn = EngagedConfirmationCircleButton("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)
|
||||
|
||||
self._power_off_btn = BigCircleButton("icons_mici/settings/device/power.png", red=True)
|
||||
self._power_off_btn.set_click_callback(lambda: _engaged_confirmation_callback(power_off_callback, "power off"))
|
||||
|
||||
self._load_languages()
|
||||
|
||||
def language_callback():
|
||||
def selected_language_callback():
|
||||
selected_language = dlg.get_selected_option()
|
||||
ui_state.params.put("LanguageSetting", self._languages[selected_language])
|
||||
|
||||
current_language_name = ui_state.params.get("LanguageSetting")
|
||||
current_language = next(name for name, lang in self._languages.items() if lang == current_language_name)
|
||||
|
||||
dlg = BigMultiOptionDialog(list(self._languages), default=current_language, right_btn_callback=selected_language_callback)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
# lang_button = BigButton("change language", "", "icons_mici/settings/device/language.png")
|
||||
# lang_button.set_click_callback(language_callback)
|
||||
|
||||
regulatory_btn = BigButton("regulatory info", "", "icons_mici/settings/device/info.png")
|
||||
regulatory_btn = BigButton("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("driver camera preview", "", "icons_mici/settings/device/cameras.png")
|
||||
driver_cam_btn.set_click_callback(self._show_driver_camera)
|
||||
driver_cam_btn = BigButton("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("review training guide", "", "icons_mici/settings/device/info.png")
|
||||
review_training_guide_btn.set_click_callback(self._on_review_training_guide)
|
||||
review_training_guide_btn = BigButton("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())
|
||||
|
||||
self._scroller = Scroller([
|
||||
terms_btn = BigButton("terms &\nconditions", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64))
|
||||
terms_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTermsPage()))
|
||||
|
||||
self._scroller.add_widgets([
|
||||
DeviceInfoLayoutMici(),
|
||||
UpdateOpenpilotBigButton(),
|
||||
BigParamControl("automatically update starpilot", "AutomaticUpdates"),
|
||||
PairBigButton(),
|
||||
review_training_guide_btn,
|
||||
driver_cam_btn,
|
||||
reset_driver_monitoring_btn,
|
||||
# lang_button,
|
||||
terms_btn,
|
||||
regulatory_btn,
|
||||
reset_calibration_btn,
|
||||
uninstall_openpilot_btn,
|
||||
regulatory_btn,
|
||||
reboot_btn,
|
||||
self._power_off_btn,
|
||||
], snap_items=False, scroll_indicator=True, edge_shadows=True)
|
||||
|
||||
# Set up back navigation
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
# Hide power off button when onroad
|
||||
ui_state.add_offroad_transition_callback(self._offroad_transition)
|
||||
])
|
||||
|
||||
def _on_regulatory(self):
|
||||
if not self._fcc_dialog:
|
||||
self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/mici_fcc.html"))
|
||||
gui_app.set_modal_overlay(self._fcc_dialog, callback=setattr(self, '_fcc_dialog', None))
|
||||
|
||||
def _offroad_transition(self):
|
||||
self._power_off_btn.set_visible(ui_state.is_offroad())
|
||||
|
||||
def _show_driver_camera(self):
|
||||
if not self._driver_camera:
|
||||
self._driver_camera = DriverCameraDialog()
|
||||
gui_app.set_modal_overlay(self._driver_camera, callback=lambda result: setattr(self, '_driver_camera', None))
|
||||
|
||||
def _on_review_training_guide(self):
|
||||
if not self._training_guide:
|
||||
def completed_callback():
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
self._training_guide = TrainingGuide(completed_callback=completed_callback)
|
||||
gui_app.set_modal_overlay(self._training_guide, callback=lambda result: setattr(self, '_training_guide', None))
|
||||
|
||||
def _load_languages(self):
|
||||
with open(os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json")) as f:
|
||||
self._languages = json.load(f)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._scroller.render(rect)
|
||||
gui_app.push_widget(self._fcc_dialog)
|
||||
|
||||
@@ -18,7 +18,7 @@ from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigDialogBase, BigMultiOptionDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.widgets import DialogResult, Widget
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
import pyray as rl
|
||||
|
||||
@@ -75,17 +75,19 @@ def _split_param(param_value: str | None) -> list[str]:
|
||||
|
||||
class DownloadProgressDialog(BigDialogBase):
|
||||
def __init__(self, params_memory: Params, is_downloading: Callable[[], bool], cancel_callback: Callable[[], None],
|
||||
is_terminal_progress: Callable[[str], bool]):
|
||||
is_terminal_progress: Callable[[str], bool], on_close: Callable[[], None] | None = None):
|
||||
super().__init__()
|
||||
self._params_memory = params_memory
|
||||
self._is_downloading = is_downloading
|
||||
self._cancel_callback = cancel_callback
|
||||
self._is_terminal_progress = is_terminal_progress
|
||||
self._on_close = on_close
|
||||
|
||||
self._progress = 0.0
|
||||
self._status = "Downloading..."
|
||||
self._terminal_progress_since = 0.0
|
||||
self._downloading = False
|
||||
self._dismissed = False
|
||||
|
||||
self._cancel_btn = DownloadActionButton("cancel download")
|
||||
self._cancel_btn.set_click_callback(self._cancel_callback)
|
||||
@@ -140,7 +142,9 @@ class DownloadProgressDialog(BigDialogBase):
|
||||
if self._terminal_progress_since == 0.0:
|
||||
self._terminal_progress_since = time.monotonic()
|
||||
elif time.monotonic() - self._terminal_progress_since >= _DOWNLOAD_DIALOG_CLOSE_SECONDS:
|
||||
self._ret = DialogResult.CONFIRM
|
||||
if not self._dismissed:
|
||||
self._dismissed = True
|
||||
self.dismiss(self._on_close)
|
||||
else:
|
||||
self._terminal_progress_since = 0.0
|
||||
|
||||
@@ -235,8 +239,6 @@ class DownloadProgressDialog(BigDialogBase):
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
)
|
||||
|
||||
return self._ret
|
||||
|
||||
|
||||
class DownloadActionButton(Widget):
|
||||
def __init__(self, label: str):
|
||||
@@ -276,7 +278,7 @@ class DownloadActionButton(Widget):
|
||||
|
||||
class DrivingModelBigButton(BigButton):
|
||||
def __init__(self):
|
||||
super().__init__("driving model", "", "icons_mici/settings/device/lkas.png")
|
||||
super().__init__("driving model", "", gui_app.texture("icons_mici/settings/device/lkas.png", 72, 56))
|
||||
self._params = Params()
|
||||
self._params_memory = Params(memory=True)
|
||||
self._model_manager = ModelManager(self._params, self._params_memory)
|
||||
@@ -285,12 +287,17 @@ class DrivingModelBigButton(BigButton):
|
||||
self._active_job = ""
|
||||
self._manifest_last_refresh_mono = 0.0
|
||||
self._terminal_progress_since = 0.0
|
||||
self._sub_label.set_font_size(32)
|
||||
self._sub_label._scroll = True
|
||||
self._sub_label._elide = False
|
||||
self._sub_label._wrap_text = False
|
||||
|
||||
self.set_click_callback(self._open_manager_menu)
|
||||
self.refresh()
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._sub_label.reset_scroll()
|
||||
self.refresh()
|
||||
# Always fetch manifest once when this settings pane opens.
|
||||
self._maybe_refresh_manifest(force=(self._manifest_last_refresh_mono == 0.0))
|
||||
@@ -298,6 +305,31 @@ class DrivingModelBigButton(BigButton):
|
||||
def refresh(self):
|
||||
self._update_button_value()
|
||||
|
||||
def set_value(self, value: str):
|
||||
super().set_value(value)
|
||||
self._sub_label.reset_scroll()
|
||||
|
||||
def _get_label_font_size(self):
|
||||
return 42
|
||||
|
||||
def _width_hint(self) -> int:
|
||||
icon_width = self._txt_icon.width + 16 if self._txt_icon else 0
|
||||
return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - icon_width)
|
||||
|
||||
def _show_option_dialog(self, title: str, options: list[str], current: str | None, on_selected: Callable[[str], None],
|
||||
back_callback: Callable[[], None] | None = None):
|
||||
dialog_holder: dict[str, BigMultiOptionDialog] = {}
|
||||
|
||||
def on_confirm():
|
||||
on_selected(dialog_holder["dialog"].get_selected_option())
|
||||
|
||||
default_option = current if current in options else None
|
||||
dialog = BigMultiOptionDialog(options=options, default=default_option, right_btn_callback=on_confirm)
|
||||
if back_callback is not None:
|
||||
dialog.set_back_callback(back_callback)
|
||||
dialog_holder["dialog"] = dialog
|
||||
gui_app.push_widget(dialog)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
self._process_terminal_progress()
|
||||
@@ -315,8 +347,7 @@ class DrivingModelBigButton(BigButton):
|
||||
if not options:
|
||||
return
|
||||
|
||||
def on_confirm():
|
||||
value = option_dialog.get_selected_option()
|
||||
def on_selected(value: str):
|
||||
if value == "set sort mode":
|
||||
self._open_sort_mode_dialog()
|
||||
elif value == "switch model":
|
||||
@@ -328,9 +359,7 @@ class DrivingModelBigButton(BigButton):
|
||||
elif value == "refresh manifest":
|
||||
self._maybe_refresh_manifest(force=True)
|
||||
|
||||
default_option = "switch model" if "switch model" in options else options[0]
|
||||
option_dialog = BigMultiOptionDialog(options=options, default=default_option, right_btn_callback=on_confirm)
|
||||
gui_app.set_modal_overlay(option_dialog)
|
||||
self._show_option_dialog("driving model", options, None, on_selected)
|
||||
|
||||
def _open_switch_dialog(self):
|
||||
self._maybe_refresh_manifest(force=False)
|
||||
@@ -346,7 +375,8 @@ class DrivingModelBigButton(BigButton):
|
||||
return
|
||||
|
||||
current_key = self._get_current_model_key()
|
||||
self._show_model_dialog("Select Driving Model", installed, current_key, self._switch_model)
|
||||
self._show_model_dialog("Select Driving Model", installed, current_key, self._switch_model,
|
||||
back_callback=self._open_manager_menu)
|
||||
|
||||
def _open_download_dialog(self):
|
||||
if ui_state.started:
|
||||
@@ -365,7 +395,8 @@ class DrivingModelBigButton(BigButton):
|
||||
self._show_message("All models downloaded", "No additional models are available.", return_to_manager=True)
|
||||
return
|
||||
|
||||
self._show_model_dialog("Download Driving Model", missing, "", self._start_model_download)
|
||||
self._show_model_dialog("Download Driving Model", missing, "", self._start_model_download,
|
||||
back_callback=self._open_manager_menu)
|
||||
|
||||
def _download_all_missing(self):
|
||||
if ui_state.started:
|
||||
@@ -398,22 +429,20 @@ class DrivingModelBigButton(BigButton):
|
||||
options = [_SORT_MODE_LABELS[mode] for mode in _SORT_MODES]
|
||||
current_mode = self._get_sort_mode()
|
||||
|
||||
def on_confirm():
|
||||
selected_label = sort_dialog.get_selected_option()
|
||||
def on_selected(selected_label: str):
|
||||
selected_mode = _LABEL_TO_SORT_MODE.get(selected_label, _SORT_MODE_ALPHABETICAL)
|
||||
self._params.put(_SORT_MODE_PARAM, selected_mode)
|
||||
self._open_manager_menu_if_no_overlay()
|
||||
self._open_manager_menu()
|
||||
|
||||
sort_dialog = BigMultiOptionDialog(options=options, default=_SORT_MODE_LABELS[current_mode], right_btn_callback=on_confirm)
|
||||
sort_dialog.set_back_callback(self._open_manager_menu)
|
||||
gui_app.set_modal_overlay(sort_dialog)
|
||||
self._show_option_dialog("sort mode", options, _SORT_MODE_LABELS[current_mode], on_selected,
|
||||
back_callback=self._open_manager_menu)
|
||||
|
||||
def _get_sort_mode(self) -> str:
|
||||
mode = (self._params.get(_SORT_MODE_PARAM, encoding="utf-8") or "").strip()
|
||||
return mode if mode in _SORT_MODES else _SORT_MODE_ALPHABETICAL
|
||||
|
||||
def _show_model_dialog(self, title: str, entries: list[ModelEntry], current_key: str,
|
||||
on_selected: Callable[[str], None]):
|
||||
on_selected: Callable[[str], None], back_callback: Callable[[], None] | None = None):
|
||||
options, option_to_key, key_to_option = self._build_model_options(entries)
|
||||
if not options:
|
||||
self._show_message("No models available", "Refresh manifest and try again.", return_to_manager=True)
|
||||
@@ -421,15 +450,12 @@ class DrivingModelBigButton(BigButton):
|
||||
|
||||
default_option = key_to_option.get(current_key, options[0])
|
||||
|
||||
def on_confirm():
|
||||
model_key = option_to_key.get(model_dialog.get_selected_option())
|
||||
def on_dialog_selected(selected_option: str):
|
||||
model_key = option_to_key.get(selected_option)
|
||||
if model_key:
|
||||
on_selected(model_key)
|
||||
self._open_manager_menu_if_no_overlay()
|
||||
|
||||
model_dialog = BigMultiOptionDialog(options=options, default=default_option, right_btn_callback=on_confirm)
|
||||
model_dialog.set_back_callback(self._open_manager_menu)
|
||||
gui_app.set_modal_overlay(model_dialog)
|
||||
self._show_option_dialog(title, options, default_option, on_dialog_selected, back_callback=back_callback)
|
||||
|
||||
def _build_model_options(self, entries: list[ModelEntry]) -> tuple[list[str], dict[str, str], dict[str, str]]:
|
||||
# Ensure display names are unique before applying status text (date/favorite).
|
||||
@@ -712,21 +738,18 @@ class DrivingModelBigButton(BigButton):
|
||||
lower = progress.lower()
|
||||
return any(pattern in lower for pattern in _TERMINAL_PROGRESS_PATTERNS)
|
||||
|
||||
def _open_manager_menu_if_no_overlay(self):
|
||||
if gui_app._modal_overlay.overlay is None:
|
||||
self._open_manager_menu()
|
||||
|
||||
def _show_download_progress_dialog(self):
|
||||
dialog = DownloadProgressDialog(
|
||||
params_memory=self._params_memory,
|
||||
is_downloading=self._is_download_job_running,
|
||||
cancel_callback=self._cancel_download,
|
||||
is_terminal_progress=self._is_terminal_progress,
|
||||
on_close=self._open_manager_menu,
|
||||
)
|
||||
gui_app.set_modal_overlay(dialog, callback=lambda _result: self._open_manager_menu())
|
||||
gui_app.push_widget(dialog)
|
||||
|
||||
def _show_message(self, title: str, description: str, return_to_manager: bool = False):
|
||||
dialog = BigDialog(title, description)
|
||||
if return_to_manager:
|
||||
dialog.set_back_callback(self._open_manager_menu)
|
||||
gui_app.set_modal_overlay(dialog)
|
||||
gui_app.push_widget(dialog)
|
||||
|
||||
@@ -13,7 +13,8 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
|
||||
from openpilot.system.ui.lib.multilang import tr, trn, tr_noop
|
||||
from openpilot.system.ui.widgets import Widget, NavWidget
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.scroller import NavRawScrollPanel
|
||||
|
||||
TITLE = tr_noop("Firehose Mode")
|
||||
DESCRIPTION = tr_noop(
|
||||
@@ -80,12 +81,12 @@ class FirehoseLayoutBase(Widget):
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# compute total content height for scrolling
|
||||
content_height = self._measure_content_height(rect)
|
||||
scroll_offset = round(self._scroll_panel.update(rect, content_height))
|
||||
scroll_offset = self._scroll_panel.update(rect, content_height)
|
||||
|
||||
# start drawing with offset
|
||||
x = int(rect.x + 40)
|
||||
y = int(rect.y + 40 + scroll_offset)
|
||||
w = int(rect.width - 80)
|
||||
x = rect.x + 40
|
||||
y = rect.y + 40 + scroll_offset
|
||||
w = rect.width - 80
|
||||
|
||||
# Title
|
||||
title_text = tr(TITLE)
|
||||
@@ -99,7 +100,7 @@ class FirehoseLayoutBase(Widget):
|
||||
y += 20
|
||||
|
||||
# Separator
|
||||
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
||||
rl.draw_rectangle_rec(rl.Rectangle(x, y, w, 2), self.GRAY)
|
||||
y += 20
|
||||
|
||||
# Status
|
||||
@@ -115,7 +116,7 @@ class FirehoseLayoutBase(Widget):
|
||||
y += 20
|
||||
|
||||
# Separator
|
||||
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
||||
rl.draw_rectangle_rec(rl.Rectangle(x, y, w, 2), self.GRAY)
|
||||
y += 20
|
||||
|
||||
# Instructions intro
|
||||
@@ -132,9 +133,6 @@ class FirehoseLayoutBase(Widget):
|
||||
y = self._draw_wrapped_text(x, y, w, tr(answer), gui_app.font(FontWeight.ROMAN), 32, self.LIGHT_GRAY)
|
||||
y += 20
|
||||
|
||||
# return value not used by NavWidget
|
||||
return -1
|
||||
|
||||
def _draw_wrapped_text(self, x, y, width, text, font, font_size, color):
|
||||
wrapped = wrap_text(font, text, font_size, width)
|
||||
for line in wrapped:
|
||||
@@ -220,9 +218,5 @@ class FirehoseLayoutBase(Widget):
|
||||
time.sleep(self.UPDATE_INTERVAL)
|
||||
|
||||
|
||||
class FirehoseLayout(FirehoseLayoutBase, NavWidget):
|
||||
BACK_TOUCH_AREA_PERCENTAGE = 0.1
|
||||
|
||||
def __init__(self, back_callback):
|
||||
super().__init__()
|
||||
self.set_back_callback(back_callback)
|
||||
class FirehoseLayout(NavRawScrollPanel, FirehoseLayoutBase):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import hashlib
|
||||
import secrets
|
||||
import string
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
import qrcode
|
||||
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog, BigMultiOptionDialog
|
||||
from openpilot.system.hardware import PC
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
|
||||
|
||||
class GalaxyQRDialog(NavWidget):
|
||||
def __init__(self, url: str):
|
||||
super().__init__()
|
||||
self._url = url
|
||||
self._qr_texture: rl.Texture | None = None
|
||||
self._title = UnifiedLabel("pair with galaxy", font_size=48, font_weight=FontWeight.BOLD, line_height=0.8)
|
||||
self._generate_qr_code()
|
||||
|
||||
def _generate_qr_code(self) -> None:
|
||||
try:
|
||||
qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=0)
|
||||
qr.add_data(self._url)
|
||||
qr.make(fit=True)
|
||||
|
||||
pil_img = qr.make_image(fill_color="white", back_color="black").convert("RGBA")
|
||||
img_array = np.array(pil_img, dtype=np.uint8)
|
||||
|
||||
if self._qr_texture and self._qr_texture.id != 0:
|
||||
rl.unload_texture(self._qr_texture)
|
||||
|
||||
rl_image = rl.Image()
|
||||
rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data)
|
||||
rl_image.width = pil_img.width
|
||||
rl_image.height = pil_img.height
|
||||
rl_image.mipmaps = 1
|
||||
rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
|
||||
self._qr_texture = rl.load_texture_from_image(rl_image)
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"Galaxy QR generation failed: {e}")
|
||||
self._qr_texture = None
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if self._qr_texture is None:
|
||||
rl.draw_text_ex(gui_app.font(FontWeight.BOLD), "QR Code Error", rl.Vector2(rect.x + 20, rect.y + rect.height / 2 - 15), 30, 0.0, rl.RED)
|
||||
return
|
||||
|
||||
scale = rect.height / self._qr_texture.height
|
||||
pos = rl.Vector2(round(rect.x + 8), round(rect.y))
|
||||
rl.draw_texture_ex(self._qr_texture, pos, 0.0, scale, rl.WHITE)
|
||||
|
||||
label_x = rect.x + 8 + rect.height + 24
|
||||
self._title.set_max_width(int(rect.width - label_x))
|
||||
self._title.set_position(label_x, rect.y + 16)
|
||||
self._title.render()
|
||||
|
||||
def __del__(self):
|
||||
if self._qr_texture and self._qr_texture.id != 0:
|
||||
rl.unload_texture(self._qr_texture)
|
||||
|
||||
|
||||
class GalaxyBigButton(BigButton):
|
||||
_SLUG_CHARS = string.ascii_letters + string.digits
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("galaxy", "", gui_app.texture("icons_mici/settings/galaxy.png", 64, 64))
|
||||
self._galaxy_dir = Path(Paths.comma_home()) / "starpilot" / "data" / "galaxy" if PC else Path("/data/galaxy")
|
||||
self._auth_path = self._galaxy_dir / "glxyauth"
|
||||
self._session_path = self._galaxy_dir / "glxysession"
|
||||
self._slug_path = self._galaxy_dir / "glxyslug"
|
||||
|
||||
def _get_label_font_size(self):
|
||||
return 64
|
||||
|
||||
def _is_paired(self) -> bool:
|
||||
try:
|
||||
return len(self._auth_path.read_text(encoding="utf-8").strip()) == 64
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _get_slug(self) -> str:
|
||||
try:
|
||||
return self._slug_path.read_text(encoding="utf-8").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _show_qr(self):
|
||||
slug = self._get_slug()
|
||||
if not slug:
|
||||
gui_app.push_widget(BigDialog("", "Galaxy is not paired yet."))
|
||||
return
|
||||
gui_app.push_widget(GalaxyQRDialog(f"https://galaxy.firestar.link/{slug}"))
|
||||
|
||||
def _pair_with_pin(self, pin: str):
|
||||
clean_pin = str(pin or "").strip()
|
||||
if len(clean_pin) < 6:
|
||||
gui_app.push_widget(BigDialog("", "PIN must be at least 6 characters."))
|
||||
return
|
||||
|
||||
try:
|
||||
self._galaxy_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._auth_path.write_text(hashlib.sha256(clean_pin.encode("utf-8")).hexdigest(), encoding="utf-8")
|
||||
self._session_path.write_text(secrets.token_hex(32), encoding="utf-8")
|
||||
slug = "".join(secrets.choice(self._SLUG_CHARS) for _ in range(16))
|
||||
self._slug_path.write_text(slug, encoding="utf-8")
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"Galaxy pairing write failed: {e}")
|
||||
gui_app.push_widget(BigDialog("", "Failed to pair with Galaxy."))
|
||||
return
|
||||
|
||||
self._show_qr()
|
||||
|
||||
def _unpair(self):
|
||||
for path in (self._auth_path, self._session_path, self._slug_path):
|
||||
try:
|
||||
path.unlink(missing_ok=True)
|
||||
except TypeError:
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
except Exception as e:
|
||||
cloudlog.warning(f"Galaxy unpair cleanup failed for {path}: {e}")
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
if self._is_paired():
|
||||
dialog_holder: dict[str, BigMultiOptionDialog] = {}
|
||||
|
||||
def on_confirm():
|
||||
selection = dialog_holder["dialog"].get_selected_option()
|
||||
if selection == "show qr":
|
||||
self._show_qr()
|
||||
elif selection == "unpair":
|
||||
gui_app.push_widget(
|
||||
BigConfirmationDialog(
|
||||
"slide to\nunpair galaxy",
|
||||
gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64),
|
||||
self._unpair,
|
||||
red=True,
|
||||
)
|
||||
)
|
||||
|
||||
dialog = BigMultiOptionDialog(options=["show qr", "unpair"], default="show qr", right_btn_callback=on_confirm)
|
||||
dialog_holder["dialog"] = dialog
|
||||
gui_app.push_widget(dialog)
|
||||
return
|
||||
|
||||
gui_app.push_widget(BigInputDialog("enter galaxy pin...", default_text="", minimum_length=6, confirm_callback=self._pair_with_pin))
|
||||
|
||||
def _update_state(self):
|
||||
self.set_value("paired" if self._is_paired() else "pair")
|
||||
@@ -1,204 +1,60 @@
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon, normalize_ssid
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle, BigParamControl
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiIcon
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import NavWidget
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, ConnectStatus, SecurityType, normalize_ssid
|
||||
|
||||
|
||||
class NetworkPanelType(IntEnum):
|
||||
NONE = 0
|
||||
WIFI = 1
|
||||
|
||||
|
||||
class NetworkLayoutMici(NavWidget):
|
||||
def __init__(self, back_callback: Callable):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = NetworkPanelType.WIFI
|
||||
self.set_back_enabled(lambda: self._current_panel == NetworkPanelType.NONE)
|
||||
|
||||
self._wifi_manager = WifiManager()
|
||||
self._wifi_manager.set_active(False)
|
||||
self._wifi_ui = WifiUIMici(self._wifi_manager, back_callback=lambda: self._switch_to_panel(NetworkPanelType.NONE))
|
||||
|
||||
self._wifi_manager.add_callbacks(
|
||||
networks_updated=self._on_network_updated,
|
||||
)
|
||||
|
||||
_tethering_icon = "icons_mici/settings/network/tethering.png"
|
||||
|
||||
# ******** 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_toggle_btn = BigToggle("enable tethering", "", toggle_callback=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("enter password...", tethering_password, minimum_length=8,
|
||||
confirm_callback=tethering_password_callback)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
txt_tethering = gui_app.texture(_tethering_icon, 64, 53)
|
||||
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
|
||||
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
|
||||
|
||||
# ******** IP Address ********
|
||||
self._ip_address_btn = BigButton("IP Address", "Not connected")
|
||||
|
||||
# ******** Network Metered ********
|
||||
def network_metered_callback(value: str):
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
metered = {
|
||||
'default': MeteredType.UNKNOWN,
|
||||
'metered': MeteredType.YES,
|
||||
'unmetered': 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("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
class WifiNetworkButton(BigButton):
|
||||
def __init__(self, wifi_manager: WifiManager):
|
||||
self._wifi_manager = wifi_manager
|
||||
self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 28, 36)
|
||||
self._draw_lock = False
|
||||
|
||||
self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 64, 56)
|
||||
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 64, 47)
|
||||
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 64, 47)
|
||||
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47)
|
||||
|
||||
wifi_button = BigButton("wi-fi", "not connected", self._wifi_slash_txt)
|
||||
wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
|
||||
self._wifi_button = wifi_button
|
||||
|
||||
# ******** Advanced settings ********
|
||||
# ******** Roaming toggle ********
|
||||
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", toggle_callback=self._toggle_roaming)
|
||||
|
||||
# ******** APN settings ********
|
||||
self._apn_btn = BigButton("apn settings", "edit")
|
||||
self._apn_btn.set_click_callback(self._edit_apn)
|
||||
|
||||
# ******** Cellular metered toggle ********
|
||||
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered)
|
||||
|
||||
# Main scroller ----------------------------------
|
||||
self._scroller = Scroller([
|
||||
wifi_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, scroll_indicator=True, edge_shadows=True)
|
||||
|
||||
# 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)
|
||||
super().__init__("wi-fi", "not connected", self._wifi_slash_txt, scroll=True)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
# If not using prime SIM, show GSM settings and enable IPv4 forwarding
|
||||
show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE)
|
||||
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)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._current_panel = NetworkPanelType.NONE
|
||||
self._wifi_ui.show_event()
|
||||
self._scroller.show_event()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._wifi_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("enter APN", current_apn, minimum_length=0, confirm_callback=update_apn)
|
||||
gui_app.set_modal_overlay(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_network_updated(self, networks: list[Network]):
|
||||
# Update tethering state
|
||||
tethering_active = self._wifi_manager.is_tethering_active()
|
||||
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
|
||||
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_toggle_btn.set_checked(tethering_active)
|
||||
|
||||
connected_network = next((network for network in networks if network.is_connected), None)
|
||||
if connected_network is not None:
|
||||
self._wifi_button.set_value(normalize_ssid(connected_network.ssid))
|
||||
strength = round(connected_network.strength / 100 * 2)
|
||||
if strength >= 2:
|
||||
self._wifi_button.set_icon(self._wifi_full_txt)
|
||||
elif strength == 1:
|
||||
self._wifi_button.set_icon(self._wifi_medium_txt)
|
||||
else:
|
||||
self._wifi_button.set_icon(self._wifi_low_txt)
|
||||
# Update wi-fi button with ssid and ip address
|
||||
# TODO: make sure we handle hidden ssids
|
||||
wifi_state = self._wifi_manager.wifi_state
|
||||
display_network = next((n for n in self._wifi_manager.networks if n.ssid == wifi_state.ssid), None)
|
||||
if wifi_state.status == ConnectStatus.CONNECTING:
|
||||
self.set_text(normalize_ssid(wifi_state.ssid or "wi-fi"))
|
||||
self.set_value("starting" if self._wifi_manager.is_tethering_active() else "connecting...")
|
||||
elif wifi_state.status == ConnectStatus.CONNECTED:
|
||||
self.set_text(normalize_ssid(wifi_state.ssid or "wi-fi"))
|
||||
self.set_value(self._wifi_manager.ipv4_address or "obtaining IP...")
|
||||
else:
|
||||
self._wifi_button.set_value("not connected")
|
||||
self._wifi_button.set_icon(self._wifi_slash_txt)
|
||||
display_network = None
|
||||
self.set_text("wi-fi")
|
||||
self.set_value("not connected")
|
||||
|
||||
# Update IP address
|
||||
self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or "Not connected")
|
||||
|
||||
# Update network metered
|
||||
self._network_metered_btn.set_value(
|
||||
{
|
||||
MeteredType.UNKNOWN: 'default',
|
||||
MeteredType.YES: 'metered',
|
||||
MeteredType.NO: 'unmetered'
|
||||
}.get(self._wifi_manager.current_network_metered, 'default'))
|
||||
|
||||
def _switch_to_panel(self, panel_type: NetworkPanelType):
|
||||
if panel_type == NetworkPanelType.WIFI:
|
||||
self._wifi_ui.show_event()
|
||||
self._current_panel = panel_type
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._wifi_manager.process_callbacks()
|
||||
|
||||
if self._current_panel == NetworkPanelType.WIFI:
|
||||
self._wifi_ui.render(rect)
|
||||
if display_network is not None:
|
||||
strength = WifiIcon.get_strength_icon_idx(display_network.strength)
|
||||
self.set_icon(self._wifi_full_txt if strength == 2 else self._wifi_medium_txt if strength == 1 else self._wifi_low_txt)
|
||||
self._draw_lock = display_network.security_type not in (SecurityType.OPEN, SecurityType.UNSUPPORTED)
|
||||
elif self._wifi_manager.is_tethering_active():
|
||||
# takes a while to get Network
|
||||
self.set_icon(self._wifi_full_txt)
|
||||
self._draw_lock = True
|
||||
else:
|
||||
self._scroller.render(rect)
|
||||
self.set_icon(self._wifi_slash_txt)
|
||||
self._draw_lock = False
|
||||
|
||||
def _draw_content(self, btn_y: float):
|
||||
super()._draw_content(btn_y)
|
||||
# Render lock icon at lower right of wifi icon if secured
|
||||
if self._draw_lock:
|
||||
icon_x = self._rect.x + self._rect.width - 30 - self._txt_icon.width
|
||||
icon_y = btn_y + 30
|
||||
lock_x = icon_x + self._txt_icon.width - self._lock_txt.width + 7
|
||||
lock_y = icon_y + self._txt_icon.height - self._lock_txt.height + 8
|
||||
rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
def should_show_forget_button(network=None, *, is_saved: bool = False, is_connected: bool = False) -> bool:
|
||||
if network is not None:
|
||||
return bool(network.is_saved or network.is_connected)
|
||||
|
||||
return bool(is_saved or is_connected)
|
||||
@@ -0,0 +1,154 @@
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network import WifiNetworkButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType
|
||||
|
||||
|
||||
class NetworkLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._wifi_manager = WifiManager()
|
||||
self._wifi_manager.set_active(False)
|
||||
self._wifi_ui = WifiUIMici(self._wifi_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._tethering_password_btn.set_enabled(False)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_active(checked)
|
||||
|
||||
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
|
||||
|
||||
def tethering_password_callback(password: str):
|
||||
if password:
|
||||
self._tethering_toggle_btn.set_enabled(False)
|
||||
self._tethering_password_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_password(password)
|
||||
|
||||
def tethering_password_clicked():
|
||||
tethering_password = self._wifi_manager.tethering_password
|
||||
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
|
||||
confirm_callback=tethering_password_callback)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
txt_tethering = gui_app.texture("icons_mici/settings/network/tethering.png", 64, 54)
|
||||
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
|
||||
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
|
||||
|
||||
# ******** Network Metered ********
|
||||
def network_metered_callback(value: str):
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
metered = {
|
||||
'default': MeteredType.UNKNOWN,
|
||||
'metered': MeteredType.YES,
|
||||
'unmetered': 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("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
|
||||
self._wifi_button = WifiNetworkButton(self._wifi_manager)
|
||||
self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui))
|
||||
|
||||
# ******** Advanced settings ********
|
||||
# ******** Roaming toggle ********
|
||||
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", toggle_callback=self._toggle_roaming)
|
||||
|
||||
# ******** APN settings ********
|
||||
self._apn_btn = BigButton("apn settings", "edit")
|
||||
self._apn_btn.set_click_callback(self._edit_apn)
|
||||
|
||||
# ******** Cellular metered toggle ********
|
||||
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered)
|
||||
|
||||
# Main scroller ----------------------------------
|
||||
self._scroller.add_widgets([
|
||||
self._wifi_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,
|
||||
# */
|
||||
])
|
||||
|
||||
# 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)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
# If not using prime SIM, show GSM settings and enable IPv4 forwarding
|
||||
show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE)
|
||||
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)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._wifi_manager.set_active(True)
|
||||
|
||||
# Process wifi callbacks while at any point in the nav stack
|
||||
gui_app.add_nav_stack_tick(self._wifi_manager.process_callbacks)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._wifi_manager.set_active(False)
|
||||
|
||||
gui_app.remove_nav_stack_tick(self._wifi_manager.process_callbacks)
|
||||
|
||||
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("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_network_updated(self, networks: list[Network]):
|
||||
# Update tethering state
|
||||
tethering_active = self._wifi_manager.is_tethering_active()
|
||||
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
|
||||
self._tethering_toggle_btn.set_enabled(True)
|
||||
self._tethering_password_btn.set_enabled(True)
|
||||
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
|
||||
self._tethering_toggle_btn.set_checked(tethering_active)
|
||||
|
||||
# Update network metered
|
||||
self._network_metered_btn.set_value(
|
||||
{
|
||||
MeteredType.UNKNOWN: 'default',
|
||||
MeteredType.YES: 'metered',
|
||||
MeteredType.NO: 'unmetered'
|
||||
}.get(self._wifi_manager.current_network_metered, 'default'))
|
||||
@@ -4,407 +4,339 @@ import pyray as rl
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigInputDialog, BigDialogOptionButton, BigConfirmationDialogV2
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog, BigConfirmationDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, LABEL_COLOR
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
|
||||
from openpilot.system.ui.widgets import Widget, NavWidget
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.action_state import should_show_forget_button
|
||||
|
||||
|
||||
def normalize_ssid(ssid: str) -> str:
|
||||
return ssid.replace("’", "'") # for iPhone hotspots
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, normalize_ssid
|
||||
|
||||
|
||||
class LoadingAnimation(Widget):
|
||||
def _render(self, _):
|
||||
cx = int(self._rect.x + 70)
|
||||
cy = int(self._rect.y + self._rect.height / 2 - 50)
|
||||
RADIUS = 8
|
||||
SPACING = 24 # center-to-center: diameter (16) + gap (8)
|
||||
Y_MAG = 11.2
|
||||
|
||||
y_mag = 20
|
||||
anim_scale = 5
|
||||
spacing = 28
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
w = self.SPACING * 2 + self.RADIUS * 2
|
||||
h = self.RADIUS * 2 + int(self.Y_MAG)
|
||||
self.set_rect(rl.Rectangle(0, 0, w, h))
|
||||
|
||||
def _render(self, _):
|
||||
# Balls rest at bottom center; bounce upward
|
||||
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 = cx - spacing + i * spacing
|
||||
y = int(cy + min(math.sin((rl.get_time() - i * 0.2) * anim_scale) * y_mag, 0))
|
||||
alpha = int(np.interp(cy - y, [0, y_mag], [255 * 0.45, 255 * 0.9]))
|
||||
rl.draw_circle(x, y, 10, rl.Color(255, 255, 255, alpha))
|
||||
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):
|
||||
def __init__(self, network: Network):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, 89, 64))
|
||||
self.set_rect(rl.Rectangle(0, 0, 48 + 5, 36 + 5))
|
||||
|
||||
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 89, 64)
|
||||
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 89, 64)
|
||||
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 89, 64)
|
||||
self._lock_txt = gui_app.texture("icons_mici/settings/network/new/lock.png", 23, 32)
|
||||
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 | None = None
|
||||
self._scale = 1.0
|
||||
self._network: Network = network
|
||||
self._network_missing = False # if network disappeared from scan results
|
||||
|
||||
def set_current_network(self, network: Network):
|
||||
def update_network(self, network: Network):
|
||||
self._network = network
|
||||
|
||||
def set_scale(self, scale: float):
|
||||
self._scale = scale
|
||||
def set_network_missing(self, missing: bool):
|
||||
self._network_missing = missing
|
||||
|
||||
@staticmethod
|
||||
def get_strength_icon_idx(strength: int) -> int:
|
||||
return round(strength / 100 * 2)
|
||||
|
||||
def _render(self, _):
|
||||
if self._network is None:
|
||||
return
|
||||
|
||||
# Determine which wifi strength icon to use
|
||||
strength = round(self._network.strength / 100 * 2)
|
||||
if strength == 2:
|
||||
strength = self.get_strength_icon_idx(self._network.strength)
|
||||
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
|
||||
|
||||
icon_x = int(self._rect.x + (self._rect.width - strength_icon.width * self._scale) // 2)
|
||||
icon_y = int(self._rect.y + (self._rect.height - strength_icon.height * self._scale) // 2)
|
||||
rl.draw_texture_ex(strength_icon, (icon_x, icon_y), 0.0, self._scale, rl.WHITE)
|
||||
rl.draw_texture_ex(strength_icon, (self._rect.x, self._rect.y + self._rect.height - strength_icon.height), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
# Render lock icon at lower right of wifi icon if secured
|
||||
if self._network.security_type not in (SecurityType.OPEN, SecurityType.UNSUPPORTED):
|
||||
lock_scale = self._scale * 1.1
|
||||
lock_x = int(icon_x + 1 + strength_icon.width * self._scale - self._lock_txt.width * lock_scale / 2)
|
||||
lock_y = int(icon_y + 1 + strength_icon.height * self._scale - self._lock_txt.height * lock_scale / 2)
|
||||
rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, lock_scale, rl.WHITE)
|
||||
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 WifiItem(BigDialogOptionButton):
|
||||
LEFT_MARGIN = 20
|
||||
class WifiButton(BigButton):
|
||||
LABEL_PADDING = 98
|
||||
LABEL_WIDTH = 402 - 98 - 28 # button width - left padding - right padding
|
||||
SUB_LABEL_WIDTH = 402 - BigButton.LABEL_HORIZONTAL_PADDING * 2
|
||||
|
||||
def __init__(self, network: Network):
|
||||
super().__init__(network.ssid)
|
||||
|
||||
self.set_rect(rl.Rectangle(0, 0, gui_app.width, self.HEIGHT))
|
||||
|
||||
self._selected_txt = gui_app.texture("icons_mici/settings/network/new/wifi_selected.png", 48, 96)
|
||||
def __init__(self, network: Network, wifi_manager: WifiManager):
|
||||
super().__init__(normalize_ssid(network.ssid), scroll=True)
|
||||
|
||||
self._network = network
|
||||
self._wifi_icon = WifiIcon()
|
||||
self._wifi_icon.set_current_network(network)
|
||||
|
||||
def set_current_network(self, network: Network):
|
||||
self._network = network
|
||||
self._wifi_icon.set_current_network(network)
|
||||
|
||||
def _render(self, _):
|
||||
if self._network.is_connected:
|
||||
selected_x = int(self._rect.x - self._selected_txt.width / 2)
|
||||
selected_y = int(self._rect.y + (self._rect.height - self._selected_txt.height) / 2)
|
||||
rl.draw_texture(self._selected_txt, selected_x, selected_y, rl.WHITE)
|
||||
|
||||
self._wifi_icon.set_scale((1.0 if self._selected else 0.65) * 0.7)
|
||||
self._wifi_icon.render(rl.Rectangle(
|
||||
self._rect.x + self.LEFT_MARGIN,
|
||||
self._rect.y,
|
||||
self.SELECTED_HEIGHT,
|
||||
self._rect.height
|
||||
))
|
||||
|
||||
if self._selected:
|
||||
self._label.set_font_size(self.SELECTED_HEIGHT)
|
||||
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
self._label.set_font_weight(FontWeight.DISPLAY)
|
||||
else:
|
||||
self._label.set_font_size(self.HEIGHT)
|
||||
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58)))
|
||||
self._label.set_font_weight(FontWeight.DISPLAY_REGULAR)
|
||||
|
||||
label_offset = self.LEFT_MARGIN + self._wifi_icon.rect.width + 20
|
||||
label_rect = rl.Rectangle(self._rect.x + label_offset, self._rect.y, self._rect.width - label_offset, self._rect.height)
|
||||
self._label.set_text(normalize_ssid(self._network.ssid))
|
||||
self._label.render(label_rect)
|
||||
|
||||
|
||||
class ConnectButton(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._bg_txt = gui_app.texture("icons_mici/settings/network/new/connect_button.png", 410, 100)
|
||||
self._bg_pressed_txt = gui_app.texture("icons_mici/settings/network/new/connect_button_pressed.png", 410, 100)
|
||||
self._bg_full_txt = gui_app.texture("icons_mici/settings/network/new/full_connect_button.png", 520, 100)
|
||||
self._bg_full_pressed_txt = gui_app.texture("icons_mici/settings/network/new/full_connect_button_pressed.png", 520, 100)
|
||||
|
||||
self._full: bool = False
|
||||
|
||||
self._label = UnifiedLabel("", 36, FontWeight.MEDIUM, rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
|
||||
@property
|
||||
def full(self) -> bool:
|
||||
return self._full
|
||||
|
||||
def set_full(self, full: bool):
|
||||
self._full = full
|
||||
self.set_rect(rl.Rectangle(0, 0, 520 if self._full else 410, 100))
|
||||
|
||||
def set_label(self, text: str):
|
||||
self._label.set_text(text)
|
||||
|
||||
def _render(self, _):
|
||||
if self._full:
|
||||
bg_txt = self._bg_full_pressed_txt if self.is_pressed and self.enabled else self._bg_full_txt
|
||||
else:
|
||||
bg_txt = self._bg_pressed_txt if self.is_pressed and self.enabled else self._bg_txt
|
||||
|
||||
rl.draw_texture(bg_txt, int(self._rect.x), int(self._rect.y), rl.WHITE)
|
||||
|
||||
self._label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9) if self.enabled else int(255 * 0.9 * 0.65)))
|
||||
self._label.render(self._rect)
|
||||
|
||||
|
||||
class ForgetButton(Widget):
|
||||
HORIZONTAL_MARGIN = 8
|
||||
|
||||
def __init__(self, forget_network: Callable, open_network_manage_page):
|
||||
super().__init__()
|
||||
self._forget_network = forget_network
|
||||
self._open_network_manage_page = open_network_manage_page
|
||||
|
||||
self._bg_txt = gui_app.texture("icons_mici/settings/network/new/forget_button.png", 100, 100)
|
||||
self._bg_pressed_txt = gui_app.texture("icons_mici/settings/network/new/forget_button_pressed.png", 100, 100)
|
||||
self._trash_txt = gui_app.texture("icons_mici/settings/network/new/trash.png", 32, 36)
|
||||
self.set_rect(rl.Rectangle(0, 0, 100 + self.HORIZONTAL_MARGIN * 2, 100))
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
dlg = BigConfirmationDialogV2("slide to forget", "icons_mici/settings/network/new/trash.png", red=True,
|
||||
confirm_callback=self._forget_network)
|
||||
gui_app.set_modal_overlay(dlg, callback=self._open_network_manage_page)
|
||||
|
||||
def _render(self, _):
|
||||
bg_txt = self._bg_pressed_txt if self.is_pressed else self._bg_txt
|
||||
rl.draw_texture(bg_txt, int(self._rect.x + self.HORIZONTAL_MARGIN), int(self._rect.y), rl.WHITE)
|
||||
|
||||
trash_x = int(self._rect.x + (self._rect.width - self._trash_txt.width) // 2)
|
||||
trash_y = int(self._rect.y + (self._rect.height - self._trash_txt.height) // 2)
|
||||
rl.draw_texture(self._trash_txt, trash_x, trash_y, rl.WHITE)
|
||||
|
||||
|
||||
class NetworkInfoPage(NavWidget):
|
||||
def __init__(self, wifi_manager, connect_callback: Callable, forget_callback: Callable, open_network_manage_page: Callable):
|
||||
super().__init__()
|
||||
self._wifi_manager = wifi_manager
|
||||
|
||||
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
self._wifi_icon = WifiIcon(network)
|
||||
self._forget_btn = ForgetButton(self._forget_network)
|
||||
self._check_txt = gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 32, 32)
|
||||
|
||||
self._wifi_icon = WifiIcon()
|
||||
self._forget_btn = ForgetButton(lambda: forget_callback(self._network.ssid) if self._network is not None else None,
|
||||
open_network_manage_page)
|
||||
self._connect_btn = ConnectButton()
|
||||
self._connect_btn.set_click_callback(lambda: connect_callback(self._network.ssid) if self._network is not None else None)
|
||||
# Eager state (not sourced from Network)
|
||||
self._network_missing = False
|
||||
self._network_forgetting = False
|
||||
self._wrong_password = False
|
||||
|
||||
self._title = UnifiedLabel("", 64, FontWeight.DISPLAY, rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, scroll=True)
|
||||
self._subtitle = UnifiedLabel("", 36, FontWeight.ROMAN, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
|
||||
self.set_back_callback(lambda: gui_app.set_modal_overlay(None))
|
||||
|
||||
# State
|
||||
self._network: Network | None = None
|
||||
self._connecting: Callable[[], str | None] | None = None
|
||||
self._show_forget_btn = False
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._title.reset_scroll()
|
||||
|
||||
def update_networks(self, networks: dict[str, Network]):
|
||||
# update current network from latest scan results
|
||||
for ssid, network in networks.items():
|
||||
if self._network is not None and ssid == self._network.ssid:
|
||||
self.set_current_network(network)
|
||||
break
|
||||
else:
|
||||
# network disappeared, close page
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
# Modal overlays stop main UI rendering, so we need to call here
|
||||
self._wifi_manager.process_callbacks()
|
||||
|
||||
if self._network is None:
|
||||
return
|
||||
|
||||
self._show_forget_btn = should_show_forget_button(self._network)
|
||||
self._connect_btn.set_full(not self._show_forget_btn)
|
||||
if self._is_connecting:
|
||||
self._connect_btn.set_label("connecting...")
|
||||
self._connect_btn.set_enabled(False)
|
||||
elif self._network.is_connected:
|
||||
self._connect_btn.set_label("disconnect")
|
||||
self._connect_btn.set_enabled(True)
|
||||
elif self._network.security_type == SecurityType.UNSUPPORTED:
|
||||
self._connect_btn.set_label("connect")
|
||||
self._connect_btn.set_enabled(False)
|
||||
else: # saved or unknown
|
||||
self._connect_btn.set_label("connect")
|
||||
self._connect_btn.set_enabled(True)
|
||||
|
||||
self._title.set_text(normalize_ssid(self._network.ssid))
|
||||
if self._network.security_type == SecurityType.OPEN:
|
||||
self._subtitle.set_text("open")
|
||||
elif self._network.security_type == SecurityType.UNSUPPORTED:
|
||||
self._subtitle.set_text("unsupported")
|
||||
else:
|
||||
self._subtitle.set_text("secured")
|
||||
|
||||
def set_current_network(self, network: Network):
|
||||
def update_network(self, network: Network):
|
||||
self._network = network
|
||||
self._wifi_icon.set_current_network(network)
|
||||
self._wifi_icon.update_network(network)
|
||||
|
||||
def set_connecting(self, is_connecting: Callable[[], str | None]):
|
||||
self._connecting = is_connecting
|
||||
# We can assume network is not missing if got new 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 _is_connecting(self):
|
||||
if self._connecting is None or self._network is None:
|
||||
return False
|
||||
is_connecting = self._connecting() == self._network.ssid
|
||||
return is_connecting
|
||||
def network_forgetting(self) -> bool:
|
||||
return self._network_forgetting
|
||||
|
||||
def _render(self, _):
|
||||
def _forget_network(self):
|
||||
if self._network_forgetting:
|
||||
return
|
||||
|
||||
self._network_forgetting = True
|
||||
self._wifi_manager.forget_connection(self._network.ssid)
|
||||
|
||||
def on_forgotten(self):
|
||||
self._network_forgetting = 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 network(self) -> Network:
|
||||
return self._network
|
||||
|
||||
@property
|
||||
def _show_forget_btn(self):
|
||||
if self._network.is_tethering or self._network_forgetting:
|
||||
return False
|
||||
|
||||
return (self._is_saved and not self._wrong_password) or self._is_connecting
|
||||
|
||||
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
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
def _get_label_font_size(self):
|
||||
return 48
|
||||
|
||||
def _draw_content(self, btn_y: float):
|
||||
self._label.set_color(LABEL_COLOR)
|
||||
label_rect = rl.Rectangle(self._rect.x + self.LABEL_PADDING, btn_y + self.LABEL_VERTICAL_PADDING,
|
||||
self.LABEL_WIDTH, self._rect.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)
|
||||
sub_label_height = self._sub_label.get_content_height(sub_label_w)
|
||||
|
||||
if self._is_connected and not self._network_forgetting:
|
||||
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)
|
||||
|
||||
# Wifi icon
|
||||
self._wifi_icon.render(rl.Rectangle(
|
||||
self._rect.x + 32,
|
||||
self._rect.y + (self._rect.height - self._connect_btn.rect.height - self._wifi_icon.rect.height) / 2,
|
||||
self._rect.x + 30,
|
||||
btn_y + 30,
|
||||
self._wifi_icon.rect.width,
|
||||
self._wifi_icon.rect.height,
|
||||
))
|
||||
|
||||
self._title.render(rl.Rectangle(
|
||||
self._rect.x + self._wifi_icon.rect.width + 32 + 32,
|
||||
self._rect.y + 32 - 16,
|
||||
self._rect.width - (self._wifi_icon.rect.width + 32 + 32),
|
||||
64,
|
||||
))
|
||||
|
||||
self._subtitle.render(rl.Rectangle(
|
||||
self._rect.x + self._wifi_icon.rect.width + 32 + 32,
|
||||
self._rect.y + 32 + 64 - 16,
|
||||
self._rect.width - (self._wifi_icon.rect.width + 32 + 32),
|
||||
48,
|
||||
))
|
||||
|
||||
self._connect_btn.render(rl.Rectangle(
|
||||
self._rect.x + 8,
|
||||
self._rect.y + self._rect.height - self._connect_btn.rect.height,
|
||||
self._connect_btn.rect.width,
|
||||
self._connect_btn.rect.height,
|
||||
))
|
||||
|
||||
# Forget button
|
||||
if self._show_forget_btn:
|
||||
self._forget_btn.render(rl.Rectangle(
|
||||
self._rect.x + self._rect.width - self._forget_btn.rect.width,
|
||||
self._rect.y + self._rect.height - self._forget_btn.rect.height,
|
||||
btn_y + self._rect.height - self._forget_btn.rect.height,
|
||||
self._forget_btn.rect.width,
|
||||
self._forget_btn.rect.height,
|
||||
))
|
||||
|
||||
return -1
|
||||
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)
|
||||
self._forget_btn.set_touch_valid_callback(touch_callback)
|
||||
|
||||
@property
|
||||
def _is_saved(self):
|
||||
return self._wifi_manager.is_connection_saved(self._network.ssid)
|
||||
|
||||
@property
|
||||
def _is_connecting(self):
|
||||
return self._wifi_manager.connecting_to_ssid == self._network.ssid
|
||||
|
||||
@property
|
||||
def _is_connected(self):
|
||||
return self._wifi_manager.connected_ssid == self._network.ssid
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
if any((self._network_missing, self._is_connecting, self._is_connected, self._network_forgetting,
|
||||
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("forgetting...")
|
||||
elif self._is_connecting:
|
||||
self.set_value("starting..." if self._network.is_tethering else "connecting...")
|
||||
elif self._is_connected:
|
||||
self.set_value("tethering" if self._network.is_tethering else "connected")
|
||||
elif self._network_missing:
|
||||
# after connecting/connected since NM will still attempt to connect/stay connected for a while
|
||||
self.set_value("not in range")
|
||||
else:
|
||||
self.set_value("unsupported")
|
||||
|
||||
else: # saved, wrong password, or unknown
|
||||
self.set_value("wrong password" if self._wrong_password else "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)
|
||||
|
||||
|
||||
class WifiUIMici(BigMultiOptionDialog):
|
||||
# Wait this long after user interacts with widget to update network list
|
||||
INACTIVITY_TIMEOUT = 1
|
||||
class ForgetButton(Widget):
|
||||
MARGIN = 12 # bottom and right
|
||||
|
||||
def __init__(self, wifi_manager: WifiManager, back_callback: Callable):
|
||||
super().__init__([], None, None, right_btn_callback=None)
|
||||
def __init__(self, forget_network: Callable):
|
||||
super().__init__()
|
||||
self._forget_network = forget_network
|
||||
|
||||
# Set up back navigation
|
||||
self.set_back_callback(back_callback)
|
||||
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))
|
||||
|
||||
self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, self._forget_network, self._open_network_manage_page)
|
||||
self._network_info_page.set_connecting(lambda: self._connecting)
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
dlg = BigConfirmationDialog("slide to forget", 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 ScanningButton(BigButton):
|
||||
def __init__(self):
|
||||
super().__init__("", "searching for networks")
|
||||
self.set_enabled(False)
|
||||
self._loading_animation = LoadingAnimation()
|
||||
|
||||
self._wifi_manager = wifi_manager
|
||||
self._connecting: str | None = None
|
||||
self._networks: dict[str, Network] = {}
|
||||
def _draw_content(self, btn_y: float):
|
||||
super()._draw_content(btn_y)
|
||||
anim = self._loading_animation
|
||||
x = self._rect.x + self._rect.width - anim.rect.width - 40
|
||||
y = btn_y + self._rect.height - anim.rect.height - 30
|
||||
anim.set_position(x, y)
|
||||
anim.render()
|
||||
|
||||
# widget state
|
||||
self._last_interaction_time = -float('inf')
|
||||
self._restore_selection = False
|
||||
|
||||
class WifiUIMici(NavScroller):
|
||||
def __init__(self, wifi_manager: WifiManager):
|
||||
super().__init__()
|
||||
|
||||
self._scanning_btn = ScanningButton()
|
||||
|
||||
self._wifi_manager = wifi_manager
|
||||
self._networks: dict[str, Network] = {}
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@property
|
||||
def any_network_forgetting(self) -> bool:
|
||||
# TODO: deactivate before forget and add DISCONNECTING state
|
||||
return any(btn.network_forgetting for btn in self._scroller.items if isinstance(btn, WifiButton))
|
||||
|
||||
def show_event(self):
|
||||
# Call super to prepare scroller; selection scroll is handled dynamically
|
||||
# Re-sort scroller items and update from latest scan results
|
||||
super().show_event()
|
||||
self._wifi_manager.set_active(True)
|
||||
self._last_interaction_time = -float('inf')
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._wifi_manager.set_active(False)
|
||||
|
||||
def _open_network_manage_page(self, result=None):
|
||||
self._network_info_page.update_networks(self._networks)
|
||||
gui_app.set_modal_overlay(self._network_info_page)
|
||||
|
||||
def _forget_network(self, ssid: str):
|
||||
network = self._networks.get(ssid)
|
||||
if network is None:
|
||||
cloudlog.warning(f"Trying to forget unknown network: {ssid}")
|
||||
return
|
||||
|
||||
self._wifi_manager.forget_connection(network.ssid)
|
||||
self._networks = {n.ssid: n for n in self._wifi_manager.networks}
|
||||
self._update_buttons(re_sort=True)
|
||||
|
||||
def _on_network_updated(self, networks: list[Network]):
|
||||
self._networks = {network.ssid: network for network in networks}
|
||||
self._update_buttons()
|
||||
self._network_info_page.update_networks(self._networks)
|
||||
|
||||
def _update_buttons(self):
|
||||
# Don't update buttons while user is actively interacting
|
||||
if rl.get_time() - self._last_interaction_time < self.INACTIVITY_TIMEOUT:
|
||||
return
|
||||
def _update_buttons(self, re_sort: bool = False):
|
||||
# Update existing buttons, add new ones to the end
|
||||
existing = {btn.network.ssid: btn for btn in self._scroller.items if isinstance(btn, WifiButton)}
|
||||
|
||||
for network in self._networks.values():
|
||||
# pop and re-insert to eliminate stuttering on update (prevents position lost for a frame)
|
||||
network_button_idx = next((i for i, btn in enumerate(self._scroller._items) if btn.option == network.ssid), None)
|
||||
if network_button_idx is not None:
|
||||
network_button = self._scroller._items.pop(network_button_idx)
|
||||
# Update network on existing button
|
||||
network_button.set_current_network(network)
|
||||
if network.ssid in existing:
|
||||
existing[network.ssid].update_network(network)
|
||||
else:
|
||||
network_button = WifiItem(network)
|
||||
btn = WifiButton(network, self._wifi_manager)
|
||||
btn.set_click_callback(lambda ssid=network.ssid: self._connect_to_network(ssid))
|
||||
self._scroller.add_widget(btn)
|
||||
|
||||
self._scroller.add_widget(network_button)
|
||||
if re_sort:
|
||||
# Remove stale buttons and sort to match scan order, preserving eager state
|
||||
btn_map = {btn.network.ssid: btn for btn in self._scroller.items if isinstance(btn, WifiButton)}
|
||||
self._scroller.items[:] = [btn_map[ssid] for ssid in self._networks if ssid in btn_map]
|
||||
else:
|
||||
# Mark networks no longer in scan results (display handled by _update_state)
|
||||
for btn in self._scroller.items:
|
||||
if isinstance(btn, WifiButton) and btn.network.ssid not in self._networks:
|
||||
btn.set_network_missing(True)
|
||||
|
||||
# remove networks no longer present
|
||||
self._scroller._items[:] = [btn for btn in self._scroller._items if btn.option in self._networks]
|
||||
|
||||
# try to restore previous selection to prevent jumping from adding/removing/reordering buttons
|
||||
self._restore_selection = True
|
||||
# Keep scanning button at the end
|
||||
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):
|
||||
if password:
|
||||
self._connecting = ssid
|
||||
self._wifi_manager.connect_to_network(ssid, password)
|
||||
self._update_buttons()
|
||||
|
||||
def _on_option_selected(self, option: str):
|
||||
super()._on_option_selected(option)
|
||||
|
||||
if option in self._networks:
|
||||
self._network_info_page.set_current_network(self._networks[option])
|
||||
self._open_network_manage_page()
|
||||
self._wifi_manager.connect_to_network(ssid, password)
|
||||
self._move_network_to_front(ssid, scroll=True)
|
||||
|
||||
def _connect_to_network(self, ssid: str):
|
||||
network = self._networks.get(ssid)
|
||||
@@ -412,51 +344,48 @@ class WifiUIMici(BigMultiOptionDialog):
|
||||
cloudlog.warning(f"Trying to connect to unknown network: {ssid}")
|
||||
return
|
||||
|
||||
if network.is_connected:
|
||||
self._wifi_manager.disconnect_network(network.ssid)
|
||||
return
|
||||
|
||||
if network.is_saved:
|
||||
self._connecting = network.ssid
|
||||
if self._wifi_manager.is_connection_saved(network.ssid):
|
||||
self._wifi_manager.activate_connection(network.ssid)
|
||||
self._update_buttons()
|
||||
elif network.security_type == SecurityType.OPEN:
|
||||
self._connecting = network.ssid
|
||||
self._wifi_manager.connect_to_network(network.ssid, "")
|
||||
self._update_buttons()
|
||||
else:
|
||||
self._on_need_auth(network.ssid, False)
|
||||
return
|
||||
|
||||
self._move_network_to_front(ssid, scroll=True)
|
||||
|
||||
def _on_need_auth(self, ssid, incorrect_password=True):
|
||||
hint = "incorrect password..." if incorrect_password else "enter password..."
|
||||
dlg = BigInputDialog(hint, "", minimum_length=8,
|
||||
if incorrect_password:
|
||||
for btn in self._scroller.items:
|
||||
if isinstance(btn, WifiButton) and btn.network.ssid == ssid:
|
||||
btn.set_wrong_password()
|
||||
break
|
||||
return
|
||||
|
||||
dlg = BigInputDialog("enter password...", "", minimum_length=8,
|
||||
confirm_callback=lambda _password: self._connect_with_password(ssid, _password))
|
||||
# go back to the manage network page
|
||||
gui_app.set_modal_overlay(dlg, self._open_network_manage_page)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
def _on_activated(self):
|
||||
self._connecting = None
|
||||
def _on_forgotten(self, ssid):
|
||||
# For eager UI forget
|
||||
for btn in self._scroller.items:
|
||||
if isinstance(btn, WifiButton) and btn.network.ssid == ssid:
|
||||
btn.on_forgotten()
|
||||
|
||||
def _on_forgotten(self):
|
||||
self._connecting = None
|
||||
def _move_network_to_front(self, ssid: str | None, scroll: bool = False):
|
||||
# Move connecting/connected network to the front with animation
|
||||
front_btn_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
|
||||
|
||||
def _on_disconnected(self):
|
||||
self._connecting = None
|
||||
if front_btn_idx is not None and front_btn_idx > 0:
|
||||
self._scroller.move_item(front_btn_idx, 0)
|
||||
|
||||
if scroll:
|
||||
# Scroll to the new position of the network
|
||||
self._scroller.scroll_to(self._scroller.scroll_panel.get_offset(), smooth=True)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if self.is_pressed:
|
||||
self._last_interaction_time = rl.get_time()
|
||||
|
||||
def _render(self, _):
|
||||
# Update Scroller layout and restore current selection whenever buttons are updated, before first render
|
||||
current_selection = self.get_selected_option()
|
||||
if self._restore_selection and current_selection in self._networks:
|
||||
self._scroller._layout()
|
||||
BigMultiOptionDialog._on_option_selected(self, current_selection)
|
||||
self._restore_selection = None
|
||||
|
||||
super()._render(_)
|
||||
|
||||
if not self._networks:
|
||||
self._loading_animation.render(self._rect)
|
||||
self._move_network_to_front(self._wifi_manager.wifi_state.ssid)
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network import NetworkLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton, GalaxyBigButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.driving_model import DrivingModelBigButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.network_layout import NetworkLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.driving_model import DrivingModelBigButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.galaxy import GalaxyBigButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.visuals import VisualsLayoutMici
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.widgets import Widget, NavWidget
|
||||
|
||||
|
||||
class PanelType(IntEnum):
|
||||
TOGGLES = 0
|
||||
NETWORK = 1
|
||||
DEVICE = 2
|
||||
DEVELOPER = 3
|
||||
USER_MANUAL = 4
|
||||
FIREHOSE = 5
|
||||
|
||||
|
||||
@dataclass
|
||||
class PanelInfo:
|
||||
name: str
|
||||
instance: Widget
|
||||
class SettingsBigButton(BigButton):
|
||||
def _get_label_font_size(self):
|
||||
return 64
|
||||
|
||||
|
||||
class ForceDriveStateBigButton(BigMultiToggle):
|
||||
@@ -37,6 +22,12 @@ class ForceDriveStateBigButton(BigMultiToggle):
|
||||
self._params = Params()
|
||||
self.refresh()
|
||||
|
||||
def _get_label_font_size(self):
|
||||
return 40
|
||||
|
||||
def _width_hint(self) -> int:
|
||||
return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width - 20)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
self._apply_mode(self.value)
|
||||
@@ -61,92 +52,51 @@ class ForceDriveStateBigButton(BigMultiToggle):
|
||||
self.set_value("off")
|
||||
|
||||
|
||||
class SettingsLayout(NavWidget):
|
||||
class SettingsLayout(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._current_panel = None # PanelType.DEVICE
|
||||
|
||||
toggles_btn = BigButton("toggles", "", "icons_mici/settings/toggles_icon.png")
|
||||
toggles_btn.set_click_callback(lambda: self._set_current_panel(PanelType.TOGGLES))
|
||||
network_btn = BigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png")
|
||||
network_btn.set_click_callback(lambda: self._set_current_panel(PanelType.NETWORK))
|
||||
device_btn = BigButton("device", "", "icons_mici/settings/device_icon.png")
|
||||
device_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVICE))
|
||||
developer_btn = BigButton("developer", "", "icons_mici/settings/developer_icon.png")
|
||||
developer_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVELOPER))
|
||||
toggles_panel = TogglesLayoutMici()
|
||||
toggles_btn = SettingsBigButton("toggles", "", gui_app.texture("icons_mici/settings.png", 64, 64))
|
||||
toggles_btn.set_click_callback(lambda: gui_app.push_widget(toggles_panel))
|
||||
|
||||
firehose_btn = BigButton("firehose", "", "icons_mici/settings/comma_icon.png")
|
||||
firehose_btn.set_click_callback(lambda: self._set_current_panel(PanelType.FIREHOSE))
|
||||
network_panel = NetworkLayoutMici()
|
||||
network_btn = SettingsBigButton("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))
|
||||
|
||||
visuals_panel = VisualsLayoutMici()
|
||||
visuals_btn = SettingsBigButton("visuals", "", gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64))
|
||||
visuals_btn.set_click_callback(lambda: gui_app.push_widget(visuals_panel))
|
||||
|
||||
device_panel = DeviceLayoutMici()
|
||||
device_btn = SettingsBigButton("device", "", gui_app.texture("icons_mici/settings/device_icon.png", 72, 58))
|
||||
device_btn.set_click_callback(lambda: gui_app.push_widget(device_panel))
|
||||
|
||||
developer_panel = DeveloperLayoutMici()
|
||||
developer_btn = SettingsBigButton("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._force_drive_state_btn = ForceDriveStateBigButton()
|
||||
self._driving_model_btn = DrivingModelBigButton()
|
||||
galaxy_btn = GalaxyBigButton()
|
||||
|
||||
self._scroller = Scroller([
|
||||
self._scroller.add_widgets([
|
||||
toggles_btn,
|
||||
network_btn,
|
||||
self._force_drive_state_btn,
|
||||
self._driving_model_btn,
|
||||
device_btn,
|
||||
self._driving_model_btn,
|
||||
visuals_btn,
|
||||
galaxy_btn,
|
||||
PairBigButton(),
|
||||
GalaxyBigButton(),
|
||||
#BigDialogButton("manual", "", "icons_mici/settings/manual_icon.png", "Check out the mici user\nmanual at comma.ai/setup"),
|
||||
firehose_btn,
|
||||
developer_btn,
|
||||
], snap_items=False, scroll_indicator=True, edge_shadows=True)
|
||||
|
||||
# Set up back navigation
|
||||
self.set_back_callback(self.close_settings)
|
||||
self.set_back_enabled(lambda: self._current_panel is None)
|
||||
|
||||
self._panels = {
|
||||
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayoutMici(back_callback=lambda: self._set_current_panel(None))),
|
||||
PanelType.NETWORK: PanelInfo("Network", NetworkLayoutMici(back_callback=lambda: self._set_current_panel(None))),
|
||||
PanelType.DEVICE: PanelInfo("Device", DeviceLayoutMici(back_callback=lambda: self._set_current_panel(None))),
|
||||
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayoutMici(back_callback=lambda: self._set_current_panel(None))),
|
||||
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout(back_callback=lambda: self._set_current_panel(None))),
|
||||
}
|
||||
])
|
||||
|
||||
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
# Callbacks
|
||||
self._close_callback: Callable | None = None
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._force_drive_state_btn.refresh()
|
||||
self._driving_model_btn.refresh()
|
||||
self._set_current_panel(None)
|
||||
self._scroller.show_event()
|
||||
if self._current_panel is not None:
|
||||
self._panels[self._current_panel].instance.show_event()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
if self._current_panel is not None:
|
||||
self._panels[self._current_panel].instance.hide_event()
|
||||
|
||||
def set_callbacks(self, on_close: Callable):
|
||||
self._close_callback = on_close
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if self._current_panel is not None:
|
||||
self._draw_current_panel()
|
||||
else:
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _draw_current_panel(self):
|
||||
panel = self._panels[self._current_panel]
|
||||
panel.instance.render(self._rect)
|
||||
|
||||
def _set_current_panel(self, panel_type: PanelType | None):
|
||||
if panel_type != self._current_panel:
|
||||
if self._current_panel is not None:
|
||||
self._panels[self._current_panel].instance.hide_event()
|
||||
self._current_panel = panel_type
|
||||
if self._current_panel is not None:
|
||||
self._panels[self._current_panel].instance.show_event()
|
||||
|
||||
def close_settings(self):
|
||||
if self._close_callback:
|
||||
self._close_callback()
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from cereal import log
|
||||
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigParamControl, BigMultiParamToggle
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import NavWidget
|
||||
from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants
|
||||
|
||||
|
||||
class TogglesLayoutMici(NavWidget):
|
||||
def __init__(self, back_callback: Callable):
|
||||
class TogglesLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"])
|
||||
self._safe_mode_btn = BigParamControl("safe mode", "SafeMode", toggle_callback=restart_needed_callback)
|
||||
@@ -27,7 +23,7 @@ class TogglesLayoutMici(NavWidget):
|
||||
record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback)
|
||||
enable_openpilot = BigParamControl("enable openpilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
|
||||
|
||||
self._scroller = Scroller([
|
||||
self._scroller.add_widgets([
|
||||
self._personality_toggle,
|
||||
self._safe_mode_btn,
|
||||
self._experimental_btn,
|
||||
@@ -37,7 +33,7 @@ class TogglesLayoutMici(NavWidget):
|
||||
record_front,
|
||||
record_mic,
|
||||
enable_openpilot,
|
||||
], snap_items=False, scroll_indicator=True, edge_shadows=True)
|
||||
])
|
||||
|
||||
# Toggle lists
|
||||
self._refresh_toggles = (
|
||||
@@ -72,7 +68,6 @@ class TogglesLayoutMici(NavWidget):
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
self._update_toggles()
|
||||
|
||||
def _update_toggles(self):
|
||||
@@ -105,6 +100,3 @@ class TogglesLayoutMici(NavWidget):
|
||||
# Refresh toggles from params to mirror external changes
|
||||
for key, item in self._refresh_toggles:
|
||||
item.set_checked(ui_state.params.get_bool(key))
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._scroller.render(rect)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigParamControl
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigMultiOptionDialog
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
|
||||
CAMERA_VIEW_LABELS = ["Auto", "Driver", "Standard", "Wide"]
|
||||
|
||||
|
||||
class CameraViewBigButton(BigButton):
|
||||
def __init__(self):
|
||||
super().__init__("camera view", "", gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64))
|
||||
self._params = Params()
|
||||
self.set_click_callback(self._show_selector)
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
current_idx = self._params.get_int("CameraView", return_default=True, default=3)
|
||||
current_idx = max(0, min(current_idx, len(CAMERA_VIEW_LABELS) - 1))
|
||||
self.set_value(CAMERA_VIEW_LABELS[current_idx].lower())
|
||||
|
||||
def _show_selector(self):
|
||||
current_idx = self._params.get_int("CameraView", return_default=True, default=3)
|
||||
current_idx = max(0, min(current_idx, len(CAMERA_VIEW_LABELS) - 1))
|
||||
dialog_holder: dict[str, BigMultiOptionDialog] = {}
|
||||
|
||||
def on_confirm():
|
||||
try:
|
||||
idx = CAMERA_VIEW_LABELS.index(dialog_holder["dialog"].get_selected_option())
|
||||
except ValueError:
|
||||
gui_app.push_widget(BigDialog("", "Invalid camera view"))
|
||||
return
|
||||
self._params.put_int("CameraView", idx)
|
||||
self.refresh()
|
||||
|
||||
dialog = BigMultiOptionDialog(options=CAMERA_VIEW_LABELS, default=CAMERA_VIEW_LABELS[current_idx], right_btn_callback=on_confirm)
|
||||
dialog_holder["dialog"] = dialog
|
||||
gui_app.push_widget(dialog)
|
||||
|
||||
|
||||
class VisualsLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._camera_view_btn = CameraViewBigButton()
|
||||
self._driver_camera_btn = BigParamControl("driver camera on reverse", "DriverCamera")
|
||||
self._stopped_timer_btn = BigParamControl("stopped timer", "StoppedTimer")
|
||||
self._speed_limit_signs_btn = BigParamControl("speed limit signs", "ShowSpeedLimits")
|
||||
self._slc_confirmation_btn = BigParamControl("confirm new speed limits", "SLCConfirmation")
|
||||
self._slc_confirmation_lower_btn = BigParamControl("confirm lower limits", "SLCConfirmationLower")
|
||||
self._slc_confirmation_higher_btn = BigParamControl("confirm higher limits", "SLCConfirmationHigher")
|
||||
|
||||
self._scroller.add_widgets([
|
||||
self._camera_view_btn,
|
||||
self._driver_camera_btn,
|
||||
self._stopped_timer_btn,
|
||||
self._speed_limit_signs_btn,
|
||||
self._slc_confirmation_btn,
|
||||
self._slc_confirmation_lower_btn,
|
||||
self._slc_confirmation_higher_btn,
|
||||
])
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._refresh()
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
self._refresh()
|
||||
|
||||
def _refresh(self):
|
||||
self._camera_view_btn.refresh()
|
||||
confirmation_enabled = self._slc_confirmation_btn.params.get_bool("SLCConfirmation")
|
||||
self._slc_confirmation_lower_btn.set_visible(confirmation_enabled)
|
||||
self._slc_confirmation_higher_btn.set_visible(confirmation_enabled)
|
||||
@@ -98,8 +98,6 @@ class AlertRenderer(Widget):
|
||||
self._prev_alert: Alert | None = None
|
||||
self._text_gen_time = 0
|
||||
self._alert_text2_gen = ''
|
||||
self._last_started_frame = -1
|
||||
self._below_steer_speed_shown_this_drive = False
|
||||
|
||||
# animation filters
|
||||
# TODO: use 0.1 but with proper alert height calculation
|
||||
@@ -113,23 +111,15 @@ class AlertRenderer(Widget):
|
||||
self._load_icons()
|
||||
|
||||
def _load_icons(self):
|
||||
self._txt_turn_signal_left = gui_app.texture('icons_mici/onroad/turn_signal_left.png', 100, 91)
|
||||
self._txt_turn_signal_right = gui_app.texture('icons_mici/onroad/turn_signal_right.png', 100, 91)
|
||||
self._txt_blind_spot_left = gui_app.texture('icons_mici/onroad/blind_spot_left.png', 108, 128)
|
||||
self._txt_blind_spot_right = gui_app.texture('icons_mici/onroad/blind_spot_right.png', 108, 128)
|
||||
self._txt_turn_signal_left = gui_app.texture('icons_mici/onroad/turn_signal_left.png', 104, 96)
|
||||
self._txt_turn_signal_right = gui_app.texture('icons_mici/onroad/turn_signal_left.png', 104, 96, flip_x=True)
|
||||
self._txt_blind_spot_left = gui_app.texture('icons_mici/onroad/blind_spot_left.png', 134, 150)
|
||||
self._txt_blind_spot_right = gui_app.texture('icons_mici/onroad/blind_spot_left.png', 134, 150, flip_x=True)
|
||||
|
||||
def get_alert(self, sm: messaging.SubMaster) -> Alert | None:
|
||||
"""Generate the current alert based on selfdrive state."""
|
||||
ss = sm['selfdriveState']
|
||||
|
||||
# Reset per-drive one-shot alert state on each new onroad session.
|
||||
if ui_state.started and ui_state.started_frame != self._last_started_frame:
|
||||
self._last_started_frame = ui_state.started_frame
|
||||
self._below_steer_speed_shown_this_drive = False
|
||||
elif not ui_state.started:
|
||||
self._last_started_frame = -1
|
||||
self._below_steer_speed_shown_this_drive = False
|
||||
|
||||
# Check if selfdriveState messages have stopped arriving
|
||||
if not sm.updated['selfdriveState']:
|
||||
recv_frame = sm.recv_frame['selfdriveState']
|
||||
@@ -155,13 +145,6 @@ class AlertRenderer(Widget):
|
||||
# Return current alert
|
||||
ret = Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize.raw, status=ss.alertStatus.raw,
|
||||
visual_alert=ss.alertHudVisual, alert_type=ss.alertType)
|
||||
|
||||
# Stock-like once-per-drive minimum lateral warning behavior.
|
||||
if ret.alert_type.startswith("belowSteerSpeed/"):
|
||||
if self._below_steer_speed_shown_this_drive:
|
||||
return None
|
||||
self._below_steer_speed_shown_this_drive = True
|
||||
|
||||
self._prev_alert = ret
|
||||
return ret
|
||||
|
||||
@@ -275,8 +258,8 @@ class AlertRenderer(Widget):
|
||||
else:
|
||||
icon_alpha = int(min(self._turn_signal_alpha_filter.x, 255))
|
||||
|
||||
rl.draw_texture(alert_layout.icon.texture, pos_x, int(self._rect.y + alert_layout.icon.margin_y),
|
||||
rl.Color(255, 255, 255, int(icon_alpha * self._alpha_filter.x)))
|
||||
rl.draw_texture_ex(alert_layout.icon.texture, rl.Vector2(pos_x, self._rect.y + alert_layout.icon.margin_y), 0.0, 1.0,
|
||||
rl.Color(255, 255, 255, int(icon_alpha * self._alpha_filter.x)))
|
||||
|
||||
def _draw_background(self, alert: Alert) -> None:
|
||||
# draw top gradient for alert text at top
|
||||
|
||||
@@ -51,13 +51,9 @@ if TICI:
|
||||
|
||||
void main() {
|
||||
vec4 color = texture(texture0, fragTexCoord);
|
||||
// Keep the onroad camera feed full-color in every driving state.
|
||||
if (engaged == 1) {
|
||||
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114)); // Luma
|
||||
color.rgb = mix(vec3(gray), color.rgb, 0.2); // 20% saturation
|
||||
color.rgb = clamp((color.rgb - 0.5) * 1.2 + 0.5, 0.0, 1.0); // +20% contrast
|
||||
color.rgb = pow(color.rgb, vec3(1.0/1.28));
|
||||
} else {
|
||||
color.rgb *= 0.85; // 85% opacity
|
||||
color.rgb = color.rgb;
|
||||
}
|
||||
if (enhance_driver == 1) {
|
||||
float brightness = 1.1;
|
||||
@@ -82,12 +78,9 @@ else:
|
||||
float y = texture(texture0, fragTexCoord).r;
|
||||
vec2 uv = texture(texture1, fragTexCoord).ra - 0.5;
|
||||
vec3 rgb = vec3(y + 1.402*uv.y, y - 0.344*uv.x - 0.714*uv.y, y + 1.772*uv.x);
|
||||
// Keep the onroad camera feed full-color in every driving state.
|
||||
if (engaged == 1) {
|
||||
float gray = dot(rgb, vec3(0.299, 0.587, 0.114));
|
||||
rgb = mix(vec3(gray), rgb, 0.2); // 20% saturation
|
||||
rgb = clamp((rgb - 0.5) * 1.2 + 0.5, 0.0, 1.0); // +20% contrast
|
||||
} else {
|
||||
rgb *= 0.85; // 85% opacity
|
||||
rgb = rgb;
|
||||
}
|
||||
// TODO: the images out of camerad need some more correction and
|
||||
// the ui should apply a gamma curve for the device display
|
||||
@@ -324,8 +317,10 @@ class CameraView(Widget):
|
||||
|
||||
def _update_texture_color_filtering(self):
|
||||
self._engaged_val[0] = 1 if ui_state.status != UIStatus.DISENGAGED else 0
|
||||
rl.set_shader_value(self.shader, self._engaged_loc, self._engaged_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT)
|
||||
rl.set_shader_value(self.shader, self._enhance_driver_loc, self._enhance_driver_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT)
|
||||
if self._engaged_loc >= 0:
|
||||
rl.set_shader_value(self.shader, self._engaged_loc, self._engaged_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT)
|
||||
if self._enhance_driver_loc >= 0:
|
||||
rl.set_shader_value(self.shader, self._enhance_driver_loc, self._enhance_driver_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT)
|
||||
|
||||
def _ensure_connection(self) -> bool:
|
||||
if not self.client.is_connected():
|
||||
|
||||
@@ -34,8 +34,14 @@ class ConfidenceBall(Widget):
|
||||
if self._demo:
|
||||
return
|
||||
|
||||
self._confidence_filter.update((1 - max(ui_state.sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs or [1])) *
|
||||
(1 - max(ui_state.sm['modelV2'].meta.disengagePredictions.steerOverrideProbs or [1])))
|
||||
lateral_ui_active = ui_state.status != UIStatus.DISENGAGED or ui_state.always_on_lateral_active
|
||||
|
||||
# animate status dot in from bottom
|
||||
if not lateral_ui_active:
|
||||
self._confidence_filter.update(-0.5)
|
||||
else:
|
||||
self._confidence_filter.update((1 - max(ui_state.sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs or [1])) *
|
||||
(1 - max(ui_state.sm['modelV2'].meta.disengagePredictions.steerOverrideProbs or [1])))
|
||||
|
||||
def _render(self, _):
|
||||
content_rect = rl.Rectangle(
|
||||
@@ -50,7 +56,7 @@ class ConfidenceBall(Widget):
|
||||
dot_height = self._rect.y + dot_height
|
||||
|
||||
# confidence zones
|
||||
if ui_state.status != UIStatus.OVERRIDE or self._demo:
|
||||
if ui_state.status == UIStatus.ENGAGED or ui_state.always_on_lateral_active or self._demo:
|
||||
if self._confidence_filter.x > 0.5:
|
||||
top_dot_color = rl.Color(0, 255, 204, 255)
|
||||
bottom_dot_color = rl.Color(0, 255, 38, 255)
|
||||
@@ -65,6 +71,10 @@ class ConfidenceBall(Widget):
|
||||
top_dot_color = rl.Color(255, 255, 255, 255)
|
||||
bottom_dot_color = rl.Color(82, 82, 82, 255)
|
||||
|
||||
else:
|
||||
top_dot_color = rl.Color(50, 50, 50, 255)
|
||||
bottom_dot_color = rl.Color(13, 13, 13, 255)
|
||||
|
||||
draw_circle_gradient(content_rect.x + content_rect.width - status_dot_radius,
|
||||
dot_height, status_dot_radius,
|
||||
top_dot_color, bottom_dot_color)
|
||||
|
||||
@@ -7,7 +7,8 @@ from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.selfdrive.selfdrived.events import EVENTS, ET
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import NavWidget
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
|
||||
EventName = log.OnroadEvent.EventName
|
||||
@@ -24,19 +25,15 @@ class DriverCameraView(CameraView):
|
||||
return base
|
||||
|
||||
|
||||
class DriverCameraDialog(NavWidget):
|
||||
def __init__(self, no_escape=False):
|
||||
class BaseDriverCameraDialog(Widget):
|
||||
# Not a NavWidget so training guide can use this without back navigation
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._camera_view = DriverCameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER)
|
||||
self.driver_state_renderer = DriverStateRenderer(lines=True)
|
||||
self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 200, 200))
|
||||
self.driver_state_renderer.load_icons()
|
||||
self._pm: messaging.PubMaster | None = None
|
||||
if not no_escape:
|
||||
# TODO: this can grow unbounded, should be given some thought
|
||||
device.add_interactive_timeout_callback(lambda: gui_app.set_modal_overlay(None))
|
||||
self.set_back_callback(lambda: gui_app.set_modal_overlay(None))
|
||||
self.set_back_enabled(not no_escape)
|
||||
|
||||
# Load eye icons
|
||||
self._eye_fill_texture = None
|
||||
@@ -85,7 +82,7 @@ class DriverCameraDialog(NavWidget):
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
rl.end_scissor_mode()
|
||||
self._publish_alert_sound(None)
|
||||
return -1
|
||||
return
|
||||
|
||||
driver_data = self._draw_face_detection(rect)
|
||||
if driver_data is not None:
|
||||
@@ -103,7 +100,7 @@ class DriverCameraDialog(NavWidget):
|
||||
self._render_dm_alerts(rect)
|
||||
|
||||
rl.end_scissor_mode()
|
||||
return -1
|
||||
return
|
||||
|
||||
def _publish_alert_sound(self, dm_state):
|
||||
"""Publish selfdriveState with only alertSound field set"""
|
||||
@@ -217,13 +214,20 @@ class DriverCameraDialog(NavWidget):
|
||||
rl.draw_texture_v(self._eye_fill_texture, (eye_x, eye_y), rl.Color(255, 255, 255, int(255 * fill_opacity)))
|
||||
|
||||
|
||||
class DriverCameraDialog(NavWidget, BaseDriverCameraDialog):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# TODO: this can grow unbounded, should be given some thought
|
||||
device.add_interactive_timeout_callback(gui_app.pop_widget)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
gui_app.init_window("Driver Camera View (mici)")
|
||||
|
||||
driver_camera_view = DriverCameraDialog()
|
||||
gui_app.push_widget(driver_camera_view)
|
||||
try:
|
||||
for _ in gui_app.render():
|
||||
ui_state.update()
|
||||
driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
finally:
|
||||
driver_camera_view.close()
|
||||
|
||||
@@ -61,7 +61,7 @@ class DriverStateRenderer(Widget):
|
||||
self._dm_cone = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_cone.png", cone_and_person_size, cone_and_person_size)
|
||||
center_size = round(36 / self.BASE_SIZE * self._rect.width)
|
||||
self._dm_center = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_center.png", center_size, center_size)
|
||||
self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", self._rect.width, self._rect.height)
|
||||
self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", int(self._rect.width), int(self._rect.height))
|
||||
|
||||
def set_should_draw(self, should_draw: bool):
|
||||
self._should_draw = should_draw
|
||||
@@ -88,15 +88,14 @@ class DriverStateRenderer(Widget):
|
||||
if DEBUG:
|
||||
rl.draw_rectangle_lines_ex(self._rect, 1, rl.RED)
|
||||
|
||||
rl.draw_texture(self._dm_background,
|
||||
int(self._rect.x),
|
||||
int(self._rect.y),
|
||||
rl.Color(255, 255, 255, int(255 * self._fade_filter.x)))
|
||||
rl.draw_texture_ex(self._dm_background,
|
||||
rl.Vector2(self._rect.x, self._rect.y), 0.0, 1.0,
|
||||
rl.Color(255, 255, 255, int(255 * self._fade_filter.x)))
|
||||
|
||||
rl.draw_texture(self._dm_person,
|
||||
int(self._rect.x + (self._rect.width - self._dm_person.width) / 2),
|
||||
int(self._rect.y + (self._rect.height - self._dm_person.height) / 2),
|
||||
rl.Color(255, 255, 255, int(255 * 0.9 * self._fade_filter.x)))
|
||||
rl.draw_texture_ex(self._dm_person,
|
||||
rl.Vector2(self._rect.x + (self._rect.width - self._dm_person.width) / 2,
|
||||
self._rect.y + (self._rect.height - self._dm_person.height) / 2), 0.0, 1.0,
|
||||
rl.Color(255, 255, 255, int(255 * 0.9 * self._fade_filter.x)))
|
||||
|
||||
if self.effective_active:
|
||||
source_rect = rl.Rectangle(0, 0, self._dm_cone.width, self._dm_cone.height)
|
||||
|
||||
@@ -264,12 +264,13 @@ class HudRenderer(Widget):
|
||||
|
||||
def _draw_steering_wheel(self, rect: rl.Rectangle) -> None:
|
||||
wheel_txt = self._txt_wheel_critical if self._show_wheel_critical else self._txt_wheel
|
||||
lateral_ui_active = ui_state.status == UIStatus.ENGAGED or ui_state.always_on_lateral_active
|
||||
|
||||
if self._show_wheel_critical:
|
||||
self._wheel_alpha_filter.update(255)
|
||||
self._wheel_y_filter.update(0)
|
||||
else:
|
||||
if ui_state.status == UIStatus.DISENGAGED:
|
||||
if not lateral_ui_active and ui_state.status == UIStatus.DISENGAGED:
|
||||
self._wheel_alpha_filter.update(0)
|
||||
self._wheel_y_filter.update(wheel_txt.height / 2)
|
||||
else:
|
||||
|
||||
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.selfdrive.ui.mici.onroad import blend_colors
|
||||
from openpilot.selfdrive.ui.mici.onroad.starpilot_status import get_border_color, get_path_edge_color
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
@@ -331,7 +331,8 @@ class ModelRenderer(Widget):
|
||||
if not self._path.projected_points.size:
|
||||
return
|
||||
|
||||
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control
|
||||
lateral_ui_active = ui_state.status == UIStatus.ENGAGED or ui_state.always_on_lateral_active
|
||||
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control or ui_state.always_on_lateral_active
|
||||
self._blend_filter.update(int(allow_throttle))
|
||||
|
||||
if self._experimental_mode:
|
||||
@@ -345,6 +346,8 @@ class ModelRenderer(Widget):
|
||||
# Blend throttle/no throttle colors based on transition
|
||||
blend_factor = round(self._blend_filter.x * 100) / 100
|
||||
blended_colors = self._blend_colors(NO_THROTTLE_COLORS, THROTTLE_COLORS, blend_factor)
|
||||
if lateral_ui_active and blend_factor < 1.0:
|
||||
blended_colors = self._blend_colors(blended_colors, THROTTLE_COLORS, 0.65)
|
||||
gradient = Gradient(
|
||||
start=(0.0, 1.0), # Bottom of path
|
||||
end=(0.0, 0.0), # Top of path
|
||||
|
||||
@@ -145,6 +145,9 @@ def arc_bar_pts(cx: float, cy: float,
|
||||
return pts
|
||||
|
||||
|
||||
DEFAULT_MAX_LAT_ACCEL = 3.0 # m/s^2
|
||||
|
||||
|
||||
class TorqueBar(Widget):
|
||||
def __init__(self, demo: bool = False):
|
||||
super().__init__()
|
||||
@@ -165,16 +168,23 @@ class TorqueBar(Widget):
|
||||
controls_state = ui_state.sm['controlsState']
|
||||
car_state = ui_state.sm['carState']
|
||||
live_parameters = ui_state.sm['liveParameters']
|
||||
lateral_acceleration = controls_state.curvature * car_state.vEgo ** 2 - live_parameters.roll * ACCELERATION_DUE_TO_GRAVITY
|
||||
# TODO: pull from carparams
|
||||
max_lateral_acceleration = 3
|
||||
car_control = ui_state.sm['carControl']
|
||||
|
||||
# from selfdrived
|
||||
# Include lateral accel error in estimated torque utilization
|
||||
actual_lateral_accel = controls_state.curvature * car_state.vEgo ** 2
|
||||
desired_lateral_accel = controls_state.desiredCurvature * car_state.vEgo ** 2
|
||||
accel_diff = (desired_lateral_accel - actual_lateral_accel)
|
||||
|
||||
self._torque_filter.update(min(max(lateral_acceleration / max_lateral_acceleration + accel_diff, -1), 1))
|
||||
# Include road roll in estimated torque utilization
|
||||
# Roll is less accurate near standstill, so reduce its effect at low speed
|
||||
roll_compensation = live_parameters.roll * ACCELERATION_DUE_TO_GRAVITY * np.interp(car_state.vEgo, [5, 15], [0.0, 1.0])
|
||||
lateral_acceleration = actual_lateral_accel - roll_compensation
|
||||
max_lateral_acceleration = ui_state.CP.maxLateralAccel if ui_state.CP else DEFAULT_MAX_LAT_ACCEL
|
||||
|
||||
if not car_control.latActive:
|
||||
self._torque_filter.update(0.0)
|
||||
else:
|
||||
self._torque_filter.update(np.clip((lateral_acceleration + accel_diff) / max_lateral_acceleration, -1, 1))
|
||||
else:
|
||||
self._torque_filter.update(-ui_state.sm['carOutput'].actuatorsOutput.torque)
|
||||
|
||||
@@ -182,16 +192,17 @@ class TorqueBar(Widget):
|
||||
# adjust y pos with torque
|
||||
torque_line_offset = np.interp(abs(self._torque_filter.x), [0.5, 1], [22, 26])
|
||||
torque_line_height = np.interp(abs(self._torque_filter.x), [0.5, 1], [14, 56])
|
||||
lateral_ui_active = ui_state.status == UIStatus.ENGAGED or ui_state.always_on_lateral_active
|
||||
|
||||
# animate alpha and angle span
|
||||
if not self._demo:
|
||||
self._torque_line_alpha_filter.update(1.0)
|
||||
self._torque_line_alpha_filter.update(lateral_ui_active or ui_state.status == UIStatus.OVERRIDE)
|
||||
else:
|
||||
self._torque_line_alpha_filter.update(1.0)
|
||||
|
||||
torque_line_bg_alpha = np.interp(abs(self._torque_filter.x), [0.5, 1.0], [0.25, 0.5])
|
||||
torque_line_bg_color = rl.Color(255, 255, 255, int(255 * torque_line_bg_alpha * self._torque_line_alpha_filter.x))
|
||||
if ui_state.status != UIStatus.ENGAGED and not self._demo:
|
||||
if not lateral_ui_active and ui_state.status != UIStatus.OVERRIDE and not self._demo:
|
||||
torque_line_bg_color = rl.Color(255, 255, 255, int(255 * 0.15 * self._torque_line_alpha_filter.x))
|
||||
|
||||
# draw curved line polygon torque bar
|
||||
@@ -234,7 +245,7 @@ class TorqueBar(Widget):
|
||||
max(0, abs(self._torque_filter.x) - 0.75) * 4,
|
||||
)
|
||||
|
||||
if ui_state.status != UIStatus.ENGAGED and not self._demo:
|
||||
if not lateral_ui_active and ui_state.status != UIStatus.OVERRIDE and not self._demo:
|
||||
start_color = end_color = rl.Color(255, 255, 255, int(255 * 0.35 * self._torque_line_alpha_filter.x))
|
||||
|
||||
gradient = Gradient(
|
||||
|
||||
Executable
+119
@@ -0,0 +1,119 @@
|
||||
import pyray as rl
|
||||
rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN)
|
||||
import gc
|
||||
import weakref
|
||||
import pytest
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
# mici dialogs
|
||||
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal
|
||||
|
||||
# tici dialogs
|
||||
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog
|
||||
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow
|
||||
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.system.ui.widgets.html_render import HtmlModal
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
|
||||
# FIXME: known small leaks not worth worrying about at the moment
|
||||
KNOWN_LEAKS = {
|
||||
"openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog.DriverCameraView",
|
||||
"openpilot.selfdrive.ui.mici.layouts.onboarding.TermsPage",
|
||||
"openpilot.selfdrive.ui.mici.layouts.onboarding.TrainingGuide",
|
||||
"openpilot.selfdrive.ui.mici.layouts.onboarding.DeclinePage",
|
||||
"openpilot.selfdrive.ui.mici.layouts.onboarding.OnboardingWindow",
|
||||
"openpilot.selfdrive.ui.onroad.driver_state.DriverStateRenderer",
|
||||
"openpilot.selfdrive.ui.onroad.driver_camera_dialog.DriverCameraDialog",
|
||||
"openpilot.selfdrive.ui.layouts.onboarding.TermsPage",
|
||||
"openpilot.selfdrive.ui.layouts.onboarding.DeclinePage",
|
||||
"openpilot.selfdrive.ui.layouts.onboarding.OnboardingWindow",
|
||||
"openpilot.system.ui.widgets.confirm_dialog.ConfirmDialog",
|
||||
"openpilot.system.ui.widgets.label.Label",
|
||||
"openpilot.system.ui.widgets.button.Button",
|
||||
"openpilot.system.ui.widgets.html_render.HtmlRenderer",
|
||||
"openpilot.system.ui.widgets.nav_widget.NavBar",
|
||||
"openpilot.selfdrive.ui.mici.layouts.settings.device.MiciFccModal",
|
||||
"openpilot.system.ui.widgets.inputbox.InputBox",
|
||||
"openpilot.system.ui.widgets.scroller_tici.Scroller",
|
||||
"openpilot.system.ui.widgets.label.UnifiedLabel",
|
||||
"openpilot.system.ui.widgets.mici_keyboard.MiciKeyboard",
|
||||
"openpilot.selfdrive.ui.mici.widgets.dialog.BigConfirmationDialog",
|
||||
"openpilot.system.ui.widgets.keyboard.Keyboard",
|
||||
"openpilot.system.ui.widgets.slider.BigSlider",
|
||||
"openpilot.selfdrive.ui.mici.widgets.dialog.BigInputDialog",
|
||||
"openpilot.system.ui.widgets.option_dialog.MultiOptionDialog",
|
||||
}
|
||||
|
||||
|
||||
def get_child_widgets(widget: Widget) -> list[Widget]:
|
||||
children = []
|
||||
for val in widget.__dict__.values():
|
||||
items = val if isinstance(val, (list, tuple)) else (val,)
|
||||
children.extend(w for w in items if isinstance(w, Widget))
|
||||
return children
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="segfaults")
|
||||
def test_dialogs_do_not_leak():
|
||||
gui_app.init_window("ref-test")
|
||||
|
||||
leaked_widgets = set()
|
||||
|
||||
for ctor in (
|
||||
# mici
|
||||
MiciDriverCameraDialog, MiciPairingDialog,
|
||||
lambda: MiciTrainingGuide(lambda: None),
|
||||
lambda: MiciOnboardingWindow(lambda: None),
|
||||
lambda: BigDialog("test", "test"),
|
||||
lambda: BigConfirmationDialog("test", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), lambda: None),
|
||||
lambda: BigInputDialog("test"),
|
||||
lambda: MiciFccModal(text="test"),
|
||||
# tici
|
||||
TiciDriverCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard,
|
||||
lambda: ConfirmDialog("test", "ok"),
|
||||
lambda: MultiOptionDialog("test", ["a", "b"]),
|
||||
lambda: HtmlModal(text="test"),
|
||||
):
|
||||
widget = ctor()
|
||||
all_refs = [weakref.ref(w) for w in get_child_widgets(widget) + [widget]]
|
||||
|
||||
del widget
|
||||
|
||||
for ref in all_refs:
|
||||
if ref() is not None:
|
||||
obj = ref()
|
||||
name = f"{type(obj).__module__}.{type(obj).__qualname__}"
|
||||
leaked_widgets.add(name)
|
||||
|
||||
print(f"\n=== Widget {name} alive after del")
|
||||
print(" Referrers:")
|
||||
for r in gc.get_referrers(obj):
|
||||
if r is obj:
|
||||
continue
|
||||
|
||||
if hasattr(r, '__self__') and r.__self__ is not obj:
|
||||
print(f" bound method: {type(r.__self__).__qualname__}.{r.__name__}")
|
||||
elif hasattr(r, '__func__'):
|
||||
print(f" method: {r.__name__}")
|
||||
else:
|
||||
print(f" {type(r).__module__}.{type(r).__qualname__}")
|
||||
del obj
|
||||
|
||||
gui_app.close()
|
||||
|
||||
unexpected = leaked_widgets - KNOWN_LEAKS
|
||||
assert not unexpected, f"New leaked widgets: {unexpected}"
|
||||
|
||||
fixed = KNOWN_LEAKS - leaked_widgets
|
||||
assert not fixed, f"These leaks are fixed, remove from KNOWN_LEAKS: {fixed}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_dialogs_do_not_leak()
|
||||
+172
-140
@@ -1,11 +1,11 @@
|
||||
import math
|
||||
import pyray as rl
|
||||
from typing import Union
|
||||
from enum import Enum
|
||||
from collections.abc import Callable
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import MiciLabel
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.scroller import DO_ZOOM
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.common.filter_simple import BounceFilter
|
||||
|
||||
@@ -16,8 +16,7 @@ except ImportError:
|
||||
|
||||
SCROLLING_SPEED_PX_S = 50
|
||||
COMPLICATION_SIZE = 36
|
||||
LABEL_COLOR = rl.WHITE
|
||||
LABEL_HORIZONTAL_PADDING = 40
|
||||
LABEL_COLOR = rl.Color(255, 255, 255, int(255 * 0.9))
|
||||
COMPLICATION_GREY = rl.Color(0xAA, 0xAA, 0xAA, 255)
|
||||
PRESSED_SCALE = 1.15 if DO_ZOOM else 1.07
|
||||
|
||||
@@ -29,50 +28,51 @@ class ScrollState(Enum):
|
||||
|
||||
|
||||
class BigCircleButton(Widget):
|
||||
def __init__(self, icon: str, red: bool = False):
|
||||
def __init__(self, icon: rl.Texture, red: bool = False, icon_offset: tuple[int, int] = (0, 0)):
|
||||
super().__init__()
|
||||
self._red = red
|
||||
self._icon_offset = icon_offset
|
||||
|
||||
# State
|
||||
self.set_rect(rl.Rectangle(0, 0, 180, 180))
|
||||
self._press_state_enabled = True
|
||||
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
|
||||
self._click_delay = 0.075
|
||||
|
||||
# Icons
|
||||
self._txt_icon = gui_app.texture(icon, 64, 53)
|
||||
self._txt_icon = icon
|
||||
self._txt_btn_disabled_bg = gui_app.texture("icons_mici/buttons/button_circle_disabled.png", 180, 180)
|
||||
|
||||
self._txt_btn_bg = gui_app.texture("icons_mici/buttons/button_circle.png", 180, 180)
|
||||
self._txt_btn_pressed_bg = gui_app.texture("icons_mici/buttons/button_circle_hover.png", 180, 180)
|
||||
self._txt_btn_pressed_bg = gui_app.texture("icons_mici/buttons/button_circle_pressed.png", 180, 180)
|
||||
|
||||
self._txt_btn_red_bg = gui_app.texture("icons_mici/buttons/button_circle_red.png", 180, 180)
|
||||
self._txt_btn_red_pressed_bg = gui_app.texture("icons_mici/buttons/button_circle_red_hover.png", 180, 180)
|
||||
self._txt_btn_red_pressed_bg = gui_app.texture("icons_mici/buttons/button_circle_red_pressed.png", 180, 180)
|
||||
|
||||
def set_enable_pressed_state(self, pressed: bool):
|
||||
self._press_state_enabled = pressed
|
||||
def _draw_content(self, btn_y: float):
|
||||
# draw icon
|
||||
icon_color = rl.Color(255, 255, 255, int(255 * 0.9)) if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
|
||||
rl.draw_texture_ex(self._txt_icon, (self._rect.x + (self._rect.width - self._txt_icon.width) / 2 + self._icon_offset[0],
|
||||
btn_y + (self._rect.height - self._txt_icon.height) / 2 + self._icon_offset[1]), 0, 1.0, icon_color)
|
||||
|
||||
def _render(self, _):
|
||||
# draw background
|
||||
txt_bg = self._txt_btn_bg if not self._red else self._txt_btn_red_bg
|
||||
if not self.enabled:
|
||||
txt_bg = self._txt_btn_disabled_bg
|
||||
elif self.is_pressed and self._press_state_enabled:
|
||||
elif self.is_pressed:
|
||||
txt_bg = self._txt_btn_pressed_bg if not self._red else self._txt_btn_red_pressed_bg
|
||||
|
||||
scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed and self._press_state_enabled else 1.0)
|
||||
scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed else 1.0)
|
||||
btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2
|
||||
btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
|
||||
rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE)
|
||||
|
||||
# draw icon
|
||||
icon_color = rl.WHITE if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
|
||||
rl.draw_texture(self._txt_icon, int(self._rect.x + (self._rect.width - self._txt_icon.width) / 2),
|
||||
int(self._rect.y + (self._rect.height - self._txt_icon.height) / 2), icon_color)
|
||||
self._draw_content(btn_y)
|
||||
|
||||
|
||||
class BigCircleToggle(BigCircleButton):
|
||||
def __init__(self, icon: str, toggle_callback: Callable = None):
|
||||
super().__init__(icon, False)
|
||||
def __init__(self, icon: rl.Texture, toggle_callback: Callable | None = None, icon_offset: tuple[int, int] = (0, 0)):
|
||||
super().__init__(icon, False, icon_offset=icon_offset)
|
||||
self._toggle_callback = toggle_callback
|
||||
|
||||
# State
|
||||
@@ -80,7 +80,7 @@ class BigCircleToggle(BigCircleButton):
|
||||
|
||||
# Icons
|
||||
self._txt_toggle_enabled = gui_app.texture("icons_mici/buttons/toggle_dot_enabled.png", 66, 66)
|
||||
self._txt_toggle_disabled = gui_app.texture("icons_mici/buttons/toggle_dot_disabled.png", 70, 70) # TODO: why discrepancy?
|
||||
self._txt_toggle_disabled = gui_app.texture("icons_mici/buttons/toggle_dot_disabled.png", 66, 66)
|
||||
|
||||
def set_checked(self, checked: bool):
|
||||
self._checked = checked
|
||||
@@ -92,62 +92,47 @@ class BigCircleToggle(BigCircleButton):
|
||||
if self._toggle_callback:
|
||||
self._toggle_callback(self._checked)
|
||||
|
||||
def _render(self, _):
|
||||
super()._render(_)
|
||||
def _draw_content(self, btn_y: float):
|
||||
super()._draw_content(btn_y)
|
||||
|
||||
# draw status icon
|
||||
rl.draw_texture(self._txt_toggle_enabled if self._checked else self._txt_toggle_disabled,
|
||||
int(self._rect.x + (self._rect.width - self._txt_toggle_enabled.width) / 2),
|
||||
int(self._rect.y + 5), rl.WHITE)
|
||||
rl.draw_texture_ex(self._txt_toggle_enabled if self._checked else self._txt_toggle_disabled,
|
||||
(self._rect.x + (self._rect.width - self._txt_toggle_enabled.width) / 2, btn_y + 5),
|
||||
0, 1.0, rl.WHITE)
|
||||
|
||||
|
||||
class BigButton(Widget):
|
||||
LABEL_HORIZONTAL_PADDING = 40
|
||||
LABEL_VERTICAL_PADDING = 23 # visually matches 30 in figma
|
||||
|
||||
"""A lightweight stand-in for the Qt BigButton, drawn & updated each frame."""
|
||||
|
||||
def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = ""):
|
||||
def __init__(self, text: str, value: str = "", icon: Union[rl.Texture, None] = None, scroll: bool = False):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, 402, 180))
|
||||
self.text = text
|
||||
self.value = value
|
||||
self.set_icon(icon)
|
||||
self._label_font_size_override: int | None = None
|
||||
self._txt_icon = icon
|
||||
self._scroll = scroll
|
||||
|
||||
self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps)
|
||||
self._click_delay = 0.075
|
||||
self._shake_start: float | None = None
|
||||
self._grow_animation_until: float | None = None
|
||||
|
||||
self._rotate_icon_t: float | None = None
|
||||
|
||||
self._label_font = gui_app.font(FontWeight.DISPLAY)
|
||||
self._value_font = gui_app.font(FontWeight.ROMAN)
|
||||
|
||||
self._label = MiciLabel(text, font_size=self._get_label_font_size(), width=int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2),
|
||||
font_weight=FontWeight.DISPLAY, color=LABEL_COLOR,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, wrap_text=True)
|
||||
self._sub_label = MiciLabel(value, font_size=COMPLICATION_SIZE, width=int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2),
|
||||
font_weight=FontWeight.ROMAN, color=COMPLICATION_GREY,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, wrap_text=True)
|
||||
self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD,
|
||||
text_color=LABEL_COLOR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, scroll=scroll,
|
||||
line_height=0.9)
|
||||
self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN,
|
||||
text_color=COMPLICATION_GREY, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||
self._update_label_layout()
|
||||
|
||||
self._load_images()
|
||||
|
||||
# internal state
|
||||
self._scroll_offset = 0 # in pixels
|
||||
self._needs_scroll = measure_text_cached(self._label_font, text, self._get_label_font_size()).x + 25 > self._rect.width
|
||||
self._scroll_timer = 0
|
||||
self._scroll_state = ScrollState.PRE_SCROLL
|
||||
|
||||
def set_icon(self, icon: Union[str, rl.Texture]):
|
||||
self._txt_icon = gui_app.texture(icon, 64, 64) if isinstance(icon, str) and len(icon) else icon
|
||||
|
||||
def _refresh_label_metrics(self):
|
||||
font_size = self._label_font_size_override if self._label_font_size_override is not None else self._get_label_font_size()
|
||||
self._label.set_font_size(font_size)
|
||||
self._needs_scroll = measure_text_cached(self._label_font, self.text, font_size).x + 25 > self._rect.width
|
||||
self._scroll_offset = 0
|
||||
self._scroll_timer = 0
|
||||
self._scroll_state = ScrollState.PRE_SCROLL
|
||||
|
||||
def _set_label_font_size_override(self, font_size: int | None):
|
||||
self._label_font_size_override = font_size
|
||||
self._refresh_label_metrics()
|
||||
def set_icon(self, icon: Union[rl.Texture, None]):
|
||||
self._txt_icon = icon
|
||||
|
||||
def set_rotate_icon(self, rotate: bool):
|
||||
if rotate and self._rotate_icon_t is not None:
|
||||
@@ -158,32 +143,37 @@ class BigButton(Widget):
|
||||
self._txt_default_bg = gui_app.texture("icons_mici/buttons/button_rectangle.png", 402, 180)
|
||||
self._txt_pressed_bg = gui_app.texture("icons_mici/buttons/button_rectangle_pressed.png", 402, 180)
|
||||
self._txt_disabled_bg = gui_app.texture("icons_mici/buttons/button_rectangle_disabled.png", 402, 180)
|
||||
self._txt_hover_bg = gui_app.texture("icons_mici/buttons/button_rectangle_hover.png", 402, 180)
|
||||
|
||||
def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None:
|
||||
super().set_touch_valid_callback(lambda: touch_callback() and self._grow_animation_until is None)
|
||||
|
||||
def _width_hint(self) -> int:
|
||||
# Single line if scrolling, so hide behind icon if exists
|
||||
icon_size = self._txt_icon.width if self._txt_icon and self._scroll and self.value else 0
|
||||
return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - icon_size)
|
||||
|
||||
def _get_label_font_size(self):
|
||||
if len(self.text) < 12:
|
||||
font_size = 64
|
||||
elif len(self.text) < 17:
|
||||
font_size = 48
|
||||
elif len(self.text) < 20:
|
||||
font_size = 42
|
||||
if len(self.text) <= 18:
|
||||
return 48
|
||||
else:
|
||||
font_size = 36
|
||||
return 42
|
||||
|
||||
def _update_label_layout(self):
|
||||
self._label.set_font_size(self._get_label_font_size())
|
||||
if self.value:
|
||||
font_size -= 20
|
||||
|
||||
return font_size
|
||||
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
else:
|
||||
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||
|
||||
def set_text(self, text: str):
|
||||
self.text = text
|
||||
self._label.set_text(text)
|
||||
self._refresh_label_metrics()
|
||||
self._update_label_layout()
|
||||
|
||||
def set_value(self, value: str):
|
||||
self.value = value
|
||||
self._sub_label.set_text(value)
|
||||
self._refresh_label_metrics()
|
||||
self._update_label_layout()
|
||||
|
||||
def get_value(self) -> str:
|
||||
return self.value
|
||||
@@ -191,64 +181,60 @@ class BigButton(Widget):
|
||||
def get_text(self):
|
||||
return self.text
|
||||
|
||||
def _update_state(self):
|
||||
# hold on text for a bit, scroll, hold again, reset
|
||||
if self._needs_scroll:
|
||||
"""`dt` should be seconds since last frame (rl.get_frame_time())."""
|
||||
# TODO: this comment is generated by GPT, prob wrong and misused
|
||||
dt = rl.get_frame_time()
|
||||
def trigger_shake(self):
|
||||
self._shake_start = rl.get_time()
|
||||
|
||||
self._scroll_timer += dt
|
||||
if self._scroll_state == ScrollState.PRE_SCROLL:
|
||||
if self._scroll_timer < 0.5:
|
||||
return
|
||||
self._scroll_state = ScrollState.SCROLLING
|
||||
self._scroll_timer = 0
|
||||
def trigger_grow_animation(self, duration: float = 0.65):
|
||||
self._grow_animation_until = rl.get_time() + duration
|
||||
|
||||
elif self._scroll_state == ScrollState.SCROLLING:
|
||||
self._scroll_offset -= SCROLLING_SPEED_PX_S * dt
|
||||
# reset when text has completely left the button + 50 px gap
|
||||
# TODO: use global constant for 30+30 px gap
|
||||
# TODO: add std Widget padding option integrated into the self._rect
|
||||
full_len = measure_text_cached(self._label_font, self.text, self._get_label_font_size()).x + 30 + 30
|
||||
if self._scroll_offset < (self._rect.width - full_len):
|
||||
self._scroll_state = ScrollState.POST_SCROLL
|
||||
self._scroll_timer = 0
|
||||
@property
|
||||
def _shake_offset(self) -> float:
|
||||
SHAKE_DURATION = 0.5
|
||||
SHAKE_AMPLITUDE = 24.0
|
||||
SHAKE_FREQUENCY = 32.0
|
||||
if self._shake_start is None:
|
||||
return 0.0
|
||||
t = rl.get_time() - self._shake_start
|
||||
if t > SHAKE_DURATION:
|
||||
return 0.0
|
||||
decay = 1.0 - t / SHAKE_DURATION
|
||||
return decay * SHAKE_AMPLITUDE * math.sin(t * SHAKE_FREQUENCY)
|
||||
|
||||
elif self._scroll_state == ScrollState.POST_SCROLL:
|
||||
# wait for a bit before starting to scroll again
|
||||
if self._scroll_timer < 0.75:
|
||||
return
|
||||
self._scroll_state = ScrollState.PRE_SCROLL
|
||||
self._scroll_timer = 0
|
||||
self._scroll_offset = 0
|
||||
def set_position(self, x: float, y: float) -> None:
|
||||
super().set_position(x + self._shake_offset, y)
|
||||
|
||||
def _handle_background(self) -> tuple[rl.Texture, float, float, float]:
|
||||
if self._grow_animation_until is not None:
|
||||
if rl.get_time() >= self._grow_animation_until:
|
||||
self._grow_animation_until = None
|
||||
|
||||
def _render(self, _):
|
||||
# draw _txt_default_bg
|
||||
txt_bg = self._txt_default_bg
|
||||
if not self.enabled:
|
||||
txt_bg = self._txt_disabled_bg
|
||||
elif self.is_pressed:
|
||||
txt_bg = self._txt_hover_bg
|
||||
txt_bg = self._txt_pressed_bg
|
||||
|
||||
scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed else 1.0)
|
||||
scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed or self._grow_animation_until is not None else 1.0)
|
||||
btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2
|
||||
btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2
|
||||
rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE)
|
||||
return txt_bg, btn_x, btn_y, scale
|
||||
|
||||
def _draw_content(self, btn_y: float):
|
||||
# LABEL ------------------------------------------------------------------
|
||||
lx = self._rect.x + LABEL_HORIZONTAL_PADDING
|
||||
ly = btn_y + self._rect.height - 33 # - 40# - self._get_label_font_size() / 2
|
||||
|
||||
if self.value:
|
||||
self._sub_label.set_position(lx, ly)
|
||||
ly -= self._sub_label.font_size + 9
|
||||
self._sub_label.render()
|
||||
label_x = self._rect.x + self.LABEL_HORIZONTAL_PADDING
|
||||
|
||||
label_color = LABEL_COLOR if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35))
|
||||
self._label.set_color(label_color)
|
||||
self._label.set_position(lx, ly)
|
||||
self._label.render()
|
||||
label_rect = rl.Rectangle(label_x, btn_y + self.LABEL_VERTICAL_PADDING, self._width_hint(),
|
||||
self._rect.height - self.LABEL_VERTICAL_PADDING * 2)
|
||||
self._label.render(label_rect)
|
||||
|
||||
if self.value:
|
||||
label_y = btn_y + self.LABEL_VERTICAL_PADDING + self._label.get_content_height(self._width_hint())
|
||||
sub_label_height = btn_y + self._rect.height - self.LABEL_VERTICAL_PADDING - label_y
|
||||
sub_label_rect = rl.Rectangle(label_x, label_y, self._width_hint(), sub_label_height)
|
||||
self._sub_label.render(sub_label_rect)
|
||||
|
||||
# ICON -------------------------------------------------------------------
|
||||
if self._txt_icon:
|
||||
@@ -256,23 +242,35 @@ class BigButton(Widget):
|
||||
if self._rotate_icon_t is not None:
|
||||
rotation = (rl.get_time() - self._rotate_icon_t) * 180
|
||||
|
||||
# drop top right with 30px padding
|
||||
# draw top right with 30px padding
|
||||
x = self._rect.x + self._rect.width - 30 - self._txt_icon.width / 2
|
||||
y = self._rect.y + 30 + self._txt_icon.height / 2
|
||||
y = btn_y + 30 + self._txt_icon.height / 2
|
||||
source_rec = rl.Rectangle(0, 0, self._txt_icon.width, self._txt_icon.height)
|
||||
dest_rec = rl.Rectangle(int(x), int(y), self._txt_icon.width, self._txt_icon.height)
|
||||
dest_rec = rl.Rectangle(x, y, self._txt_icon.width, self._txt_icon.height)
|
||||
origin = rl.Vector2(self._txt_icon.width / 2, self._txt_icon.height / 2)
|
||||
rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.WHITE)
|
||||
rl.draw_texture_pro(self._txt_icon, source_rec, dest_rec, origin, rotation, rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
|
||||
def _render(self, _):
|
||||
txt_bg, btn_x, btn_y, scale = self._handle_background()
|
||||
|
||||
if self._scroll:
|
||||
# draw black background since images are transparent
|
||||
scaled_rect = rl.Rectangle(btn_x, btn_y, self._rect.width * scale, self._rect.height * scale)
|
||||
rl.draw_rectangle_rounded(scaled_rect, 0.4, 7, rl.Color(0, 0, 0, int(255 * 0.5)))
|
||||
|
||||
self._draw_content(btn_y)
|
||||
rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE)
|
||||
else:
|
||||
rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, rl.WHITE)
|
||||
self._draw_content(btn_y)
|
||||
|
||||
|
||||
class BigToggle(BigButton):
|
||||
def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable = None):
|
||||
def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable | None = None):
|
||||
super().__init__(text, value, "")
|
||||
self._checked = initial_state
|
||||
self._toggle_callback = toggle_callback
|
||||
|
||||
self._set_label_font_size_override(48)
|
||||
|
||||
def _load_images(self):
|
||||
super()._load_images()
|
||||
self._txt_enabled_toggle = gui_app.texture("icons_mici/buttons/toggle_pill_enabled.png", 84, 66)
|
||||
@@ -290,35 +288,30 @@ class BigToggle(BigButton):
|
||||
def _draw_pill(self, x: float, y: float, checked: bool):
|
||||
# draw toggle icon top right
|
||||
if checked:
|
||||
rl.draw_texture(self._txt_enabled_toggle, int(x), int(y), rl.WHITE)
|
||||
rl.draw_texture_ex(self._txt_enabled_toggle, (x, y), 0, 1.0, rl.WHITE)
|
||||
else:
|
||||
rl.draw_texture(self._txt_disabled_toggle, int(x), int(y), rl.WHITE)
|
||||
rl.draw_texture_ex(self._txt_disabled_toggle, (x, y), 0, 1.0, rl.WHITE)
|
||||
|
||||
def _render(self, _):
|
||||
super()._render(_)
|
||||
def _draw_content(self, btn_y: float):
|
||||
super()._draw_content(btn_y)
|
||||
|
||||
x = self._rect.x + self._rect.width - self._txt_enabled_toggle.width
|
||||
y = self._rect.y
|
||||
y = btn_y
|
||||
self._draw_pill(x, y, self._checked)
|
||||
|
||||
|
||||
class BigMultiToggle(BigToggle):
|
||||
def __init__(self, text: str, options: list[str], toggle_callback: Callable = None,
|
||||
select_callback: Callable = None):
|
||||
def __init__(self, text: str, options: list[str], toggle_callback: Callable | None = None,
|
||||
select_callback: Callable | None = None):
|
||||
super().__init__(text, "", toggle_callback=toggle_callback)
|
||||
assert len(options) > 0
|
||||
self._options = options
|
||||
self._select_callback = select_callback
|
||||
|
||||
self._label.set_width(int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width))
|
||||
# Keep the title size stable when the selected option changes.
|
||||
self._set_label_font_size_override(self._get_label_font_size())
|
||||
|
||||
self.set_value(self._options[0])
|
||||
|
||||
def _get_label_font_size(self):
|
||||
font_size = super()._get_label_font_size()
|
||||
return font_size - 6
|
||||
def _width_hint(self) -> int:
|
||||
return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
@@ -328,22 +321,60 @@ class BigMultiToggle(BigToggle):
|
||||
if self._select_callback:
|
||||
self._select_callback(self.value)
|
||||
|
||||
def _render(self, _):
|
||||
BigButton._render(self, _)
|
||||
def _draw_content(self, btn_y: float):
|
||||
# don't draw pill from BigToggle
|
||||
BigButton._draw_content(self, btn_y)
|
||||
|
||||
checked_idx = self._options.index(self.value)
|
||||
|
||||
x = self._rect.x + self._rect.width - self._txt_enabled_toggle.width
|
||||
y = self._rect.y
|
||||
y = btn_y
|
||||
|
||||
for i in range(len(self._options)):
|
||||
self._draw_pill(x, y, checked_idx == i)
|
||||
y += 35
|
||||
|
||||
|
||||
class GreyBigButton(BigButton):
|
||||
"""Users should manage newlines with this class themselves"""
|
||||
|
||||
LABEL_HORIZONTAL_PADDING = 30
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_touch_valid_callback(lambda: False)
|
||||
|
||||
self._rect.width = 476
|
||||
|
||||
self._label.set_font_size(36)
|
||||
self._label.set_font_weight(FontWeight.BOLD)
|
||||
self._label.set_line_height(1.0)
|
||||
|
||||
self._sub_label.set_font_size(36)
|
||||
self._sub_label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
self._sub_label.set_font_weight(FontWeight.DISPLAY_REGULAR)
|
||||
self._sub_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE if not self._label.text else
|
||||
rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||
self._sub_label.set_line_height(0.95)
|
||||
|
||||
@property
|
||||
def LABEL_VERTICAL_PADDING(self):
|
||||
return BigButton.LABEL_VERTICAL_PADDING if self._label.text else 18
|
||||
|
||||
def _width_hint(self) -> int:
|
||||
return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2)
|
||||
|
||||
def _get_label_font_size(self):
|
||||
return 36
|
||||
|
||||
def _render(self, _):
|
||||
rl.draw_rectangle_rounded(self._rect, 0.4, 10, rl.Color(255, 255, 255, int(255 * 0.15)))
|
||||
self._draw_content(self._rect.y)
|
||||
|
||||
|
||||
class BigMultiParamToggle(BigMultiToggle):
|
||||
def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable = None,
|
||||
select_callback: Callable = None):
|
||||
def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable | None = None,
|
||||
select_callback: Callable | None = None):
|
||||
super().__init__(text, options, toggle_callback, select_callback)
|
||||
self._param = param
|
||||
|
||||
@@ -360,7 +391,7 @@ class BigMultiParamToggle(BigMultiToggle):
|
||||
|
||||
|
||||
class BigParamControl(BigToggle):
|
||||
def __init__(self, text: str, param: str, toggle_callback: Callable = None):
|
||||
def __init__(self, text: str, param: str, toggle_callback: Callable | None = None):
|
||||
super().__init__(text, "", toggle_callback=toggle_callback)
|
||||
self.param = param
|
||||
self.params = Params()
|
||||
@@ -376,8 +407,9 @@ class BigParamControl(BigToggle):
|
||||
|
||||
# TODO: param control base class
|
||||
class BigCircleParamControl(BigCircleToggle):
|
||||
def __init__(self, icon: str, param: str, toggle_callback: Callable = None):
|
||||
super().__init__(icon, toggle_callback)
|
||||
def __init__(self, icon: rl.Texture, param: str, toggle_callback: Callable | None = None,
|
||||
icon_offset: tuple[int, int] = (0, 0)):
|
||||
super().__init__(icon, toggle_callback, icon_offset=icon_offset)
|
||||
self._param = param
|
||||
self.params = Params()
|
||||
self.set_checked(self.params.get_bool(self._param, False))
|
||||
|
||||
+146
-212
@@ -1,19 +1,18 @@
|
||||
import abc
|
||||
import math
|
||||
import pyray as rl
|
||||
from typing import Union
|
||||
from typing import Union, cast
|
||||
from collections.abc import Callable
|
||||
from typing import cast
|
||||
from openpilot.system.ui.widgets import Widget, NavWidget, DialogResult
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, MouseEvent
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton, BigButton, GreyBigButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.side_button import SideButton
|
||||
|
||||
DEBUG = False
|
||||
@@ -22,162 +21,80 @@ PADDING = 20
|
||||
|
||||
|
||||
class BigDialogBase(NavWidget, abc.ABC):
|
||||
def __init__(self, right_btn: str | None = None, right_btn_callback: Callable | None = None):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._ret = DialogResult.NO_ACTION
|
||||
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
self.set_back_callback(lambda: setattr(self, '_ret', DialogResult.CANCEL))
|
||||
|
||||
self._right_btn = None
|
||||
if right_btn:
|
||||
def right_btn_callback_wrapper():
|
||||
gui_app.set_modal_overlay(None)
|
||||
if right_btn_callback:
|
||||
right_btn_callback()
|
||||
|
||||
self._right_btn = SideButton(right_btn)
|
||||
self._right_btn.set_click_callback(right_btn_callback_wrapper)
|
||||
# move to right side
|
||||
self._right_btn._rect.x = self._rect.x + self._rect.width - self._right_btn._rect.width
|
||||
|
||||
def _render(self, _) -> DialogResult:
|
||||
"""
|
||||
Allows `gui_app.set_modal_overlay(BigDialog(...))`.
|
||||
The overlay runner keeps calling until result != NO_ACTION.
|
||||
"""
|
||||
if self._right_btn:
|
||||
self._right_btn.set_position(self._right_btn._rect.x, self._rect.y)
|
||||
self._right_btn.render()
|
||||
|
||||
return self._ret
|
||||
|
||||
|
||||
class BigDialog(BigDialogBase):
|
||||
def __init__(self,
|
||||
title: str,
|
||||
description: str,
|
||||
right_btn: str | None = None,
|
||||
right_btn_callback: Callable | None = None):
|
||||
super().__init__(right_btn, right_btn_callback)
|
||||
self._title = title
|
||||
self._description = description
|
||||
def __init__(self, title: str, description: str, icon: Union[rl.Texture, None] = None):
|
||||
super().__init__()
|
||||
self._card = GreyBigButton(title, description, icon)
|
||||
|
||||
def _render(self, _) -> DialogResult:
|
||||
super()._render(_)
|
||||
|
||||
# draw title
|
||||
# TODO: we desperately need layouts
|
||||
# TODO: coming up with these numbers manually is a pain and not scalable
|
||||
# TODO: no clue what any of these numbers mean. VBox and HBox would remove all of this shite
|
||||
max_width = self._rect.width - PADDING * 2
|
||||
if self._right_btn:
|
||||
max_width -= self._right_btn._rect.width
|
||||
|
||||
title_font_size = 50
|
||||
desc_font_size = 30
|
||||
title_lines = wrap_text(gui_app.font(FontWeight.BOLD), self._title, title_font_size, int(max_width))
|
||||
if not title_lines:
|
||||
title_lines = [""]
|
||||
title_line_height = max(int(title_font_size * 1.2), int(measure_text_cached(gui_app.font(FontWeight.BOLD), "Ag", title_font_size).y))
|
||||
text_x_offset = 0
|
||||
title_x = int(self._rect.x + text_x_offset + PADDING)
|
||||
title_y = int(self._rect.y + PADDING)
|
||||
for i, line in enumerate(title_lines):
|
||||
line_rect = rl.Rectangle(
|
||||
title_x,
|
||||
title_y + i * title_line_height,
|
||||
int(max_width),
|
||||
int(title_line_height),
|
||||
)
|
||||
gui_label(line_rect, line, title_font_size, font_weight=FontWeight.BOLD,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
|
||||
# draw description
|
||||
desc_lines = wrap_text(gui_app.font(FontWeight.MEDIUM), self._description, desc_font_size, int(max_width))
|
||||
if not desc_lines:
|
||||
desc_lines = [""]
|
||||
desc_line_height = max(int(desc_font_size * 1.25), int(measure_text_cached(gui_app.font(FontWeight.MEDIUM), "Ag", desc_font_size).y))
|
||||
desc_y = max(
|
||||
int(self._rect.y + self._rect.height / 3),
|
||||
title_y + title_line_height * len(title_lines) + 22,
|
||||
)
|
||||
for i, line in enumerate(desc_lines):
|
||||
line_rect = rl.Rectangle(
|
||||
title_x,
|
||||
desc_y + i * desc_line_height,
|
||||
int(max_width),
|
||||
int(desc_line_height),
|
||||
)
|
||||
gui_label(line_rect, line, desc_font_size, font_weight=FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
|
||||
return self._ret
|
||||
def _render(self, _):
|
||||
self._card.render(rl.Rectangle(
|
||||
self._rect.x + self._rect.width / 2 - self._card.rect.width / 2,
|
||||
self._rect.y + self._rect.height / 2 - self._card.rect.height / 2,
|
||||
self._card.rect.width,
|
||||
self._card.rect.height,
|
||||
))
|
||||
|
||||
|
||||
class BigConfirmationDialogV2(BigDialogBase):
|
||||
def __init__(self, title: str, icon: str, red: bool = False,
|
||||
exit_on_confirm: bool = True,
|
||||
confirm_callback: Callable | None = None):
|
||||
class BigConfirmationDialog(BigDialogBase):
|
||||
def __init__(self, title: str, icon: rl.Texture, confirm_callback: Callable[[], None],
|
||||
exit_on_confirm: bool = True, red: bool = False):
|
||||
super().__init__()
|
||||
self._confirm_callback = confirm_callback
|
||||
self._exit_on_confirm = exit_on_confirm
|
||||
|
||||
icon_txt = gui_app.texture(icon, 64, 53)
|
||||
self._slider: BigSlider | RedBigSlider
|
||||
if red:
|
||||
self._slider = RedBigSlider(title, icon_txt, confirm_callback=self._on_confirm)
|
||||
self._slider = self._child(RedBigSlider(title, icon, confirm_callback=self._on_confirm))
|
||||
else:
|
||||
self._slider = BigSlider(title, icon_txt, confirm_callback=self._on_confirm)
|
||||
self._slider.set_enabled(lambda: not self._swiping_away)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._slider.show_event()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._slider.hide_event()
|
||||
self._slider = self._child(BigSlider(title, icon, confirm_callback=self._on_confirm))
|
||||
self._slider.set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget
|
||||
|
||||
def _on_confirm(self):
|
||||
if self._confirm_callback:
|
||||
self._confirm_callback()
|
||||
if self._exit_on_confirm:
|
||||
self._ret = DialogResult.CONFIRM
|
||||
self.dismiss(self._confirm_callback)
|
||||
elif self._confirm_callback:
|
||||
self._confirm_callback()
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if self._swiping_away and not self._slider.confirmed:
|
||||
self._slider.reset(reset_shimmer=False)
|
||||
if self.is_dismissing and not self._slider.confirmed:
|
||||
self._slider.reset()
|
||||
|
||||
def _render(self, _) -> DialogResult:
|
||||
def _render(self, _):
|
||||
self._slider.render(self._rect)
|
||||
return self._ret
|
||||
|
||||
|
||||
class BigInputDialog(BigDialogBase):
|
||||
BACK_TOUCH_AREA_PERCENTAGE = 0.2
|
||||
BACKSPACE_RATE = 25 # hz
|
||||
TEXT_INPUT_SIZE = 35
|
||||
|
||||
def __init__(self,
|
||||
hint: str,
|
||||
default_text: str = "",
|
||||
minimum_length: int = 1,
|
||||
confirm_callback: Callable[[str], None] = None):
|
||||
super().__init__(None, None)
|
||||
confirm_callback: Callable[[str], None] | None = None,
|
||||
auto_return_to_letters: str = ""):
|
||||
super().__init__()
|
||||
self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)),
|
||||
font_weight=FontWeight.MEDIUM)
|
||||
self._keyboard = MiciKeyboard()
|
||||
self._keyboard = MiciKeyboard(auto_return_to_letters=auto_return_to_letters)
|
||||
self._keyboard.set_text(default_text)
|
||||
self._keyboard.set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack + NavWidget
|
||||
self._minimum_length = minimum_length
|
||||
|
||||
self._backspace_held_time: float | None = None
|
||||
|
||||
self._backspace_img = gui_app.texture("icons_mici/settings/keyboard/backspace.png", 44, 44)
|
||||
self._backspace_img = gui_app.texture("icons_mici/settings/keyboard/backspace.png", 42, 36)
|
||||
self._backspace_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
|
||||
|
||||
self._enter_img = gui_app.texture("icons_mici/settings/keyboard/confirm.png", 44, 44)
|
||||
self._enter_img = gui_app.texture("icons_mici/settings/keyboard/enter.png", 76, 62)
|
||||
self._enter_disabled_img = gui_app.texture("icons_mici/settings/keyboard/enter_disabled.png", 76, 62)
|
||||
self._enter_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
|
||||
|
||||
# rects for top buttons
|
||||
@@ -185,14 +102,17 @@ class BigInputDialog(BigDialogBase):
|
||||
self._top_right_button_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
def confirm_callback_wrapper():
|
||||
self._ret = DialogResult.CONFIRM
|
||||
if confirm_callback:
|
||||
confirm_callback(self._keyboard.text())
|
||||
text = self._keyboard.text()
|
||||
self.dismiss((lambda: confirm_callback(text)) if confirm_callback else None)
|
||||
self._confirm_callback = confirm_callback_wrapper
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
if self.is_dismissing:
|
||||
self._backspace_held_time = None
|
||||
return
|
||||
|
||||
last_mouse_event = gui_app.last_mouse_event
|
||||
if last_mouse_event.left_down and rl.check_collision_point_rec(last_mouse_event.pos, self._top_right_button_rect) and self._backspace_img_alpha.x > 1:
|
||||
if self._backspace_held_time is None:
|
||||
@@ -206,64 +126,60 @@ class BigInputDialog(BigDialogBase):
|
||||
self._backspace_held_time = None
|
||||
|
||||
def _render(self, _):
|
||||
text_input_size = 35
|
||||
|
||||
# draw current text so far below everything. text floats left but always stays in view
|
||||
text = self._keyboard.text()
|
||||
candidate_char = self._keyboard.get_candidate_character()
|
||||
text_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), text + candidate_char or self._hint_label.text, text_input_size)
|
||||
text_x = PADDING * 2 + self._enter_img.width
|
||||
text_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), text + candidate_char or self._hint_label.text, self.TEXT_INPUT_SIZE)
|
||||
|
||||
# text needs to move left if we're at the end where right button is
|
||||
text_rect = rl.Rectangle(text_x,
|
||||
int(self._rect.y + PADDING),
|
||||
# clip width to right button when in view
|
||||
int(self._rect.width - text_x - PADDING * 2 - self._enter_img.width + 5), # TODO: why 5?
|
||||
int(text_size.y))
|
||||
|
||||
# draw rounded background for text input
|
||||
bg_block_margin = 5
|
||||
text_field_rect = rl.Rectangle(text_rect.x - bg_block_margin, text_rect.y - bg_block_margin,
|
||||
text_rect.width + bg_block_margin * 2, text_input_size + bg_block_margin * 2)
|
||||
text_x = PADDING / 2 + self._enter_img.width + PADDING
|
||||
text_field_rect = rl.Rectangle(text_x, self._rect.y + PADDING - bg_block_margin,
|
||||
self._rect.width - text_x * 2,
|
||||
text_size.y)
|
||||
|
||||
# draw text input
|
||||
# push text left with a gradient on left side if too long
|
||||
if text_size.x > text_rect.width:
|
||||
text_x -= text_size.x - text_rect.width
|
||||
if text_size.x > text_field_rect.width:
|
||||
text_x -= text_size.x - text_field_rect.width
|
||||
|
||||
rl.begin_scissor_mode(int(text_rect.x), int(text_rect.y), int(text_rect.width), int(text_rect.height))
|
||||
rl.draw_text_ex(gui_app.font(FontWeight.ROMAN), text, rl.Vector2(text_x, text_rect.y), text_input_size, 0, rl.WHITE)
|
||||
rl.begin_scissor_mode(int(text_field_rect.x), int(text_field_rect.y), int(text_field_rect.width), int(text_field_rect.height))
|
||||
rl.draw_text_ex(gui_app.font(FontWeight.ROMAN), text, rl.Vector2(text_x, text_field_rect.y), self.TEXT_INPUT_SIZE, 0, rl.WHITE)
|
||||
|
||||
# draw grayed out character user is hovering over
|
||||
if candidate_char:
|
||||
candidate_char_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), candidate_char, text_input_size)
|
||||
candidate_char_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), candidate_char, self.TEXT_INPUT_SIZE)
|
||||
rl.draw_text_ex(gui_app.font(FontWeight.ROMAN), candidate_char,
|
||||
rl.Vector2(min(text_x + text_size.x, text_rect.x + text_rect.width) - candidate_char_size.x, text_rect.y),
|
||||
text_input_size, 0, rl.Color(255, 255, 255, 128))
|
||||
rl.Vector2(min(text_x + text_size.x, text_field_rect.x + text_field_rect.width) - candidate_char_size.x, text_field_rect.y),
|
||||
self.TEXT_INPUT_SIZE, 0, rl.Color(255, 255, 255, 128))
|
||||
|
||||
rl.end_scissor_mode()
|
||||
|
||||
# draw gradient on left side to indicate more text
|
||||
if text_size.x > text_rect.width:
|
||||
rl.draw_rectangle_gradient_h(int(text_rect.x), int(text_rect.y), 80, int(text_rect.height),
|
||||
rl.BLACK, rl.BLANK)
|
||||
if text_size.x > text_field_rect.width:
|
||||
rl.draw_rectangle_gradient_ex(rl.Rectangle(text_field_rect.x, text_field_rect.y, 80, text_field_rect.height),
|
||||
rl.BLACK, rl.BLANK, rl.BLANK, rl.BLACK)
|
||||
|
||||
# draw cursor
|
||||
blink_alpha = (math.sin(rl.get_time() * 6) + 1) / 2
|
||||
if text:
|
||||
blink_alpha = (math.sin(rl.get_time() * 6) + 1) / 2
|
||||
cursor_x = min(text_x + text_size.x + 3, text_rect.x + text_rect.width)
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(int(cursor_x), int(text_rect.y), 4, int(text_size.y)),
|
||||
1, 4, rl.Color(255, 255, 255, int(255 * blink_alpha)))
|
||||
cursor_x = min(text_x + text_size.x + 3, text_field_rect.x + text_field_rect.width)
|
||||
else:
|
||||
cursor_x = text_field_rect.x - 6
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(cursor_x, text_field_rect.y, 4, text_size.y),
|
||||
1, 4, rl.Color(255, 255, 255, int(255 * blink_alpha)))
|
||||
|
||||
# draw backspace icon with nice fade
|
||||
self._backspace_img_alpha.update(255 * bool(text))
|
||||
if self._backspace_img_alpha.x > 1:
|
||||
color = rl.Color(255, 255, 255, int(self._backspace_img_alpha.x))
|
||||
rl.draw_texture(self._backspace_img, int(self._rect.width - self._enter_img.width - 15), int(text_field_rect.y), color)
|
||||
rl.draw_texture_ex(self._backspace_img, rl.Vector2(self._rect.width - self._backspace_img.width - 27, self._rect.y + 14), 0.0, 1.0, color)
|
||||
|
||||
if not text and self._hint_label.text and not candidate_char:
|
||||
# draw description if no text entered yet and not drawing candidate char
|
||||
self._hint_label.render(text_field_rect)
|
||||
hint_rect = rl.Rectangle(text_field_rect.x, text_field_rect.y,
|
||||
self._rect.width - text_field_rect.x - PADDING,
|
||||
text_field_rect.height)
|
||||
self._hint_label.render(hint_rect)
|
||||
|
||||
# TODO: move to update state
|
||||
# make rect take up entire area so it's easier to click
|
||||
@@ -271,10 +187,12 @@ class BigInputDialog(BigDialogBase):
|
||||
self._top_right_button_rect = rl.Rectangle(text_field_rect.x + text_field_rect.width, self._rect.y,
|
||||
self._rect.width - (text_field_rect.x + text_field_rect.width), self._top_left_button_rect.height)
|
||||
|
||||
self._enter_img_alpha.update(255 if (len(text) >= self._minimum_length) else 255 * 0.35)
|
||||
if self._enter_img_alpha.x > 1:
|
||||
color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x))
|
||||
rl.draw_texture(self._enter_img, int(self._rect.x + 15), int(text_field_rect.y), color)
|
||||
# draw enter button
|
||||
self._enter_img_alpha.update(255 if len(text) >= self._minimum_length else 0)
|
||||
color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x))
|
||||
rl.draw_texture_ex(self._enter_img, rl.Vector2(self._rect.x + PADDING / 2, self._rect.y), 0.0, 1.0, color)
|
||||
color = rl.Color(255, 255, 255, 255 - int(self._enter_img_alpha.x))
|
||||
rl.draw_texture_ex(self._enter_disabled_img, rl.Vector2(self._rect.x + PADDING / 2, self._rect.y), 0.0, 1.0, color)
|
||||
|
||||
# keyboard goes over everything
|
||||
self._keyboard.render(self._rect)
|
||||
@@ -282,16 +200,17 @@ class BigInputDialog(BigDialogBase):
|
||||
# draw debugging rect bounds
|
||||
if DEBUG:
|
||||
rl.draw_rectangle_lines_ex(text_field_rect, 1, rl.Color(100, 100, 100, 255))
|
||||
rl.draw_rectangle_lines_ex(text_rect, 1, rl.Color(0, 255, 0, 255))
|
||||
rl.draw_rectangle_lines_ex(self._top_right_button_rect, 1, rl.Color(0, 255, 0, 255))
|
||||
rl.draw_rectangle_lines_ex(self._top_left_button_rect, 1, rl.Color(0, 255, 0, 255))
|
||||
|
||||
return self._ret
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_press(mouse_pos)
|
||||
# TODO: need to track where press was so enter and back can activate on release rather than press
|
||||
# or turn into icon widgets :eyes_open:
|
||||
|
||||
if self.is_dismissing:
|
||||
return
|
||||
|
||||
# handle backspace icon click
|
||||
if rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect) and self._backspace_img_alpha.x > 254:
|
||||
self._keyboard.backspace()
|
||||
@@ -300,6 +219,30 @@ class BigInputDialog(BigDialogBase):
|
||||
self._confirm_callback()
|
||||
|
||||
|
||||
class BigDialogButton(BigButton):
|
||||
def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", description: str = ""):
|
||||
super().__init__(text, value, icon)
|
||||
self._description = description
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
dlg = BigDialog(self.text, self._description)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
|
||||
class BigConfirmationCircleButton(BigCircleButton):
|
||||
def __init__(self, title: str, icon: rl.Texture, confirm_callback: Callable[[], None], exit_on_confirm: bool = True,
|
||||
red: bool = False, icon_offset: tuple[int, int] = (0, 0)):
|
||||
super().__init__(icon, red, icon_offset)
|
||||
|
||||
def show_confirm_dialog():
|
||||
gui_app.push_widget(BigConfirmationDialog(title, icon, confirm_callback,
|
||||
exit_on_confirm=exit_on_confirm, red=red))
|
||||
|
||||
self.set_click_callback(show_confirm_dialog)
|
||||
|
||||
|
||||
class BigDialogOptionButton(Widget):
|
||||
HEIGHT = 64
|
||||
SELECTED_HEIGHT = 74
|
||||
@@ -308,12 +251,15 @@ class BigDialogOptionButton(Widget):
|
||||
super().__init__()
|
||||
self.option = option
|
||||
self.set_rect(rl.Rectangle(0, 0, int(gui_app.width / 2 + 220), self.HEIGHT))
|
||||
|
||||
self._selected = False
|
||||
|
||||
self._label = UnifiedLabel(option, font_size=70, text_color=rl.Color(255, 255, 255, int(255 * 0.58)),
|
||||
font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||
scroll=True)
|
||||
self._label = UnifiedLabel(
|
||||
option,
|
||||
font_size=70,
|
||||
text_color=rl.Color(255, 255, 255, int(255 * 0.58)),
|
||||
font_weight=FontWeight.DISPLAY_REGULAR,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||
scroll=True,
|
||||
)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
@@ -324,10 +270,6 @@ class BigDialogOptionButton(Widget):
|
||||
self._rect.height = self.SELECTED_HEIGHT if selected else self.HEIGHT
|
||||
|
||||
def _render(self, _):
|
||||
if DEBUG:
|
||||
rl.draw_rectangle_lines_ex(self._rect, 1, rl.Color(0, 255, 0, 255))
|
||||
|
||||
# FIXME: offset x by -45 because scroller centers horizontally
|
||||
if self._selected:
|
||||
self._label.set_font_size(self.SELECTED_HEIGHT)
|
||||
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
@@ -340,56 +282,63 @@ class BigDialogOptionButton(Widget):
|
||||
self._label.render(self._rect)
|
||||
|
||||
|
||||
class BigMultiOptionDialog(BigDialogBase):
|
||||
BACK_TOUCH_AREA_PERCENTAGE = 0.1
|
||||
class BigMultiOptionDialog(NavWidget):
|
||||
BACK_TOUCH_AREA_PERCENTAGE = 0.25
|
||||
|
||||
def __init__(self, options: list[str], default: str | None,
|
||||
right_btn: str | None = 'check', right_btn_callback: Callable[[], None] = None):
|
||||
super().__init__(right_btn, right_btn_callback=right_btn_callback)
|
||||
right_btn: str | None = "check", right_btn_callback: Callable[[], None] | None = None):
|
||||
super().__init__()
|
||||
self._options = options
|
||||
if default is not None:
|
||||
assert default in options
|
||||
if default not in options:
|
||||
default = options[0] if options else None
|
||||
|
||||
self._right_btn_callback = right_btn_callback
|
||||
self._default_option: str | None = default
|
||||
self._selected_option: str = self._default_option or (options[0] if len(options) > 0 else "")
|
||||
self._selected_option: str = self._default_option or (options[0] if options else "")
|
||||
self._last_selected_option: str = self._selected_option
|
||||
|
||||
# Widget doesn't differentiate between click and drag
|
||||
self._can_click = True
|
||||
|
||||
self._scroller = Scroller([], horizontal=False, pad_start=100, pad_end=100, spacing=0, snap_items=True)
|
||||
self._scroller = self._child(Scroller(horizontal=False, pad=100, spacing=0, snap_items=True,
|
||||
scroll_indicator=False, edge_shadows=False))
|
||||
self._scroll_inner = self._scroller._scroller
|
||||
|
||||
self._right_btn = self._child(SideButton(right_btn or "check")) if right_btn is not None else None
|
||||
if self._right_btn is not None:
|
||||
self._scroller.set_enabled(lambda: not cast(Widget, self._right_btn).is_pressed)
|
||||
self._right_btn.set_click_callback(self._confirm_selection)
|
||||
self._scroller.set_enabled(lambda: self.enabled and not self.is_dismissing and not self._right_btn.is_pressed)
|
||||
else:
|
||||
self._scroller.set_enabled(lambda: self.enabled and not self.is_dismissing)
|
||||
|
||||
for option in options:
|
||||
self._scroller.add_widget(BigDialogOptionButton(option))
|
||||
self._scroll_inner.add_widget(BigDialogOptionButton(option))
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
if self._default_option is not None:
|
||||
self._on_option_selected(self._default_option)
|
||||
|
||||
def get_selected_option(self) -> str:
|
||||
return self._selected_option
|
||||
|
||||
def _confirm_selection(self):
|
||||
self.dismiss(self._right_btn_callback)
|
||||
|
||||
def _on_option_selected(self, option: str):
|
||||
y_pos = 0.0
|
||||
for btn in self._scroller._items:
|
||||
for btn in self._scroll_inner.items:
|
||||
btn = cast(BigDialogOptionButton, btn)
|
||||
if btn.option == option:
|
||||
rect_center_y = self._rect.y + self._rect.height / 2
|
||||
if btn._selected:
|
||||
height = btn.rect.height
|
||||
else:
|
||||
# when selecting an option under current, account for changing heights
|
||||
btn_center_y = btn.rect.y + btn.rect.height / 2 # not accurate, just to determine direction
|
||||
btn_center_y = btn.rect.y + btn.rect.height / 2
|
||||
height_offset = BigDialogOptionButton.SELECTED_HEIGHT - BigDialogOptionButton.HEIGHT
|
||||
height = (BigDialogOptionButton.HEIGHT - height_offset) if rect_center_y < btn_center_y else BigDialogOptionButton.SELECTED_HEIGHT
|
||||
y_pos = rect_center_y - (btn.rect.y + height / 2)
|
||||
break
|
||||
|
||||
self._scroller.scroll_to(-y_pos)
|
||||
self._scroll_inner.scroll_to(-y_pos)
|
||||
|
||||
def _selected_option_changed(self):
|
||||
pass
|
||||
@@ -400,9 +349,7 @@ class BigMultiOptionDialog(BigDialogBase):
|
||||
|
||||
def _handle_mouse_event(self, mouse_event: MouseEvent) -> None:
|
||||
super()._handle_mouse_event(mouse_event)
|
||||
|
||||
# # TODO: add generic _handle_mouse_click handler to Widget
|
||||
if not self._scroller.scroll_panel.is_touch_valid():
|
||||
if not self._scroll_inner.scroll_panel.is_touch_valid():
|
||||
self._can_click = False
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
@@ -411,8 +358,7 @@ class BigMultiOptionDialog(BigDialogBase):
|
||||
if not self._can_click:
|
||||
return
|
||||
|
||||
# select current option
|
||||
for btn in self._scroller._items:
|
||||
for btn in self._scroll_inner.items:
|
||||
btn = cast(BigDialogOptionButton, btn)
|
||||
if btn.option == self._selected_option:
|
||||
self._on_option_selected(btn.option)
|
||||
@@ -420,39 +366,27 @@ class BigMultiOptionDialog(BigDialogBase):
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if not self.is_dismissing:
|
||||
self._nav_bar.set_alpha(1.0)
|
||||
|
||||
# get selection by whichever button is closest to center
|
||||
center_y = self._rect.y + self._rect.height / 2
|
||||
closest_btn = (None, float('inf'))
|
||||
for btn in self._scroller._items:
|
||||
closest_btn = (None, float("inf"))
|
||||
for btn in self._scroll_inner.items:
|
||||
dist_y = abs((btn.rect.y + btn.rect.height / 2) - center_y)
|
||||
if dist_y < closest_btn[1]:
|
||||
closest_btn = (btn, dist_y)
|
||||
|
||||
if closest_btn[0]:
|
||||
for btn in self._scroller._items:
|
||||
if closest_btn[0] is not None:
|
||||
for btn in self._scroll_inner.items:
|
||||
btn.set_selected(btn.option == closest_btn[0].option)
|
||||
self._selected_option = closest_btn[0].option
|
||||
|
||||
# Signal to subclasses if selection changed
|
||||
if self._selected_option != self._last_selected_option:
|
||||
self._selected_option_changed()
|
||||
self._last_selected_option = self._selected_option
|
||||
|
||||
def _render(self, _):
|
||||
super()._render(_)
|
||||
if self._right_btn is not None:
|
||||
self._right_btn.set_position(self._rect.x + self._rect.width - self._right_btn.rect.width, self._rect.y)
|
||||
self._right_btn.render()
|
||||
self._scroller.render(self._rect)
|
||||
|
||||
return self._ret
|
||||
|
||||
|
||||
class BigDialogButton(BigButton):
|
||||
def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", description: str = ""):
|
||||
super().__init__(text, value, icon)
|
||||
self._description = description
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
dlg = BigDialog(self.text, self._description)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
@@ -7,9 +7,9 @@ from openpilot.common.api import Api
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.widgets import NavWidget
|
||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.widgets.label import MiciLabel
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
|
||||
|
||||
class PairingDialog(NavWidget):
|
||||
@@ -19,14 +19,12 @@ class PairingDialog(NavWidget):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_back_callback(lambda: gui_app.set_modal_overlay(None))
|
||||
self._params = Params()
|
||||
self._qr_texture: rl.Texture | None = None
|
||||
self._last_qr_generation = float("-inf")
|
||||
|
||||
self._txt_pair = gui_app.texture("icons_mici/settings/device/pair.png", 84, 64)
|
||||
self._pair_label = MiciLabel("pair with comma connect", 48, font_weight=FontWeight.BOLD,
|
||||
color=rl.Color(255, 255, 255, int(255 * 0.9)), line_height=40, wrap_text=True)
|
||||
self._txt_pair = gui_app.texture("icons_mici/settings/device/pair.png", 33, 60)
|
||||
self._pair_label = UnifiedLabel("pair with comma connect", font_size=48, font_weight=FontWeight.BOLD, line_height=0.8)
|
||||
|
||||
def _get_pairing_url(self) -> str:
|
||||
try:
|
||||
@@ -69,24 +67,22 @@ class PairingDialog(NavWidget):
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if ui_state.prime_state.is_paired():
|
||||
self._playing_dismiss_animation = True
|
||||
if ui_state.prime_state.is_paired() and not self.is_dismissing:
|
||||
self.dismiss()
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> int:
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._check_qr_refresh()
|
||||
|
||||
self._render_qr_code()
|
||||
|
||||
label_x = self._rect.x + 8 + self._rect.height + 24
|
||||
self._pair_label.set_width(int(self._rect.width - label_x))
|
||||
self._pair_label.set_max_width(int(self._rect.width - label_x))
|
||||
self._pair_label.set_position(label_x, self._rect.y + 16)
|
||||
self._pair_label.render()
|
||||
|
||||
rl.draw_texture_ex(self._txt_pair, rl.Vector2(label_x, self._rect.y + self._rect.height - self._txt_pair.height - 16),
|
||||
0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.35)))
|
||||
|
||||
return -1
|
||||
|
||||
def _render_qr_code(self) -> None:
|
||||
if not self._qr_texture:
|
||||
error_font = gui_app.font(FontWeight.BOLD)
|
||||
@@ -96,7 +92,7 @@ class PairingDialog(NavWidget):
|
||||
return
|
||||
|
||||
scale = self._rect.height / self._qr_texture.height
|
||||
pos = rl.Vector2(self._rect.x + 8, self._rect.y)
|
||||
pos = rl.Vector2(round(self._rect.x + 8), round(self._rect.y))
|
||||
rl.draw_texture_ex(self._qr_texture, pos, 0.0, scale, rl.WHITE)
|
||||
|
||||
def __del__(self):
|
||||
@@ -107,10 +103,9 @@ class PairingDialog(NavWidget):
|
||||
if __name__ == "__main__":
|
||||
gui_app.init_window("pairing device")
|
||||
pairing = PairingDialog()
|
||||
gui_app.push_widget(pairing)
|
||||
try:
|
||||
for _ in gui_app.render():
|
||||
result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
if result != -1:
|
||||
break
|
||||
pass
|
||||
finally:
|
||||
del pairing
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants extracted from the original Qt style
|
||||
# ---------------------------------------------------------------------------
|
||||
# TODO: this should be corrected, but Scroller relies on this being incorrect :/
|
||||
WIDTH, HEIGHT = 112, 240
|
||||
|
||||
|
||||
@@ -15,17 +12,15 @@ class SideButton(Widget):
|
||||
self.type = btn_type
|
||||
self.set_rect(rl.Rectangle(0, 0, WIDTH, HEIGHT))
|
||||
|
||||
# load pre-rendered button images
|
||||
if btn_type not in ("check", "back"):
|
||||
btn_type = "back"
|
||||
btn_img_path = f"icons_mici/buttons/button_side_{btn_type}.png"
|
||||
btn_img_pressed_path = f"icons_mici/buttons/button_side_{btn_type}_pressed.png"
|
||||
self._txt_btn, self._txt_btn_back = gui_app.texture(btn_img_path, 100, 224), gui_app.texture(btn_img_pressed_path, 100, 224)
|
||||
self._txt_btn = gui_app.texture(btn_img_path, 100, 224)
|
||||
self._txt_btn_back = gui_app.texture(btn_img_pressed_path, 100, 224)
|
||||
|
||||
def _render(self, _) -> bool:
|
||||
def _render(self, _):
|
||||
x = int(self._rect.x + 12)
|
||||
y = int(self._rect.y + (self._rect.height - self._txt_btn.height) / 2)
|
||||
rl.draw_texture(self._txt_btn if not self.is_pressed else self._txt_btn_back,
|
||||
x, y, rl.WHITE)
|
||||
|
||||
rl.draw_texture(self._txt_btn if not self.is_pressed else self._txt_btn_back, x, y, rl.WHITE)
|
||||
return False
|
||||
|
||||
@@ -325,6 +325,7 @@ class AugmentedRoadView(CameraView):
|
||||
if __name__ == "__main__":
|
||||
gui_app.init_window("OnRoad Camera View")
|
||||
road_camera_view = AugmentedRoadView(ROAD_CAM)
|
||||
gui_app.push_widget(road_camera_view)
|
||||
print("***press space to switch camera view***")
|
||||
try:
|
||||
for _ in gui_app.render():
|
||||
@@ -333,6 +334,5 @@ if __name__ == "__main__":
|
||||
if WIDE_CAM in road_camera_view.available_streams:
|
||||
stream = ROAD_CAM if road_camera_view.stream_type == WIDE_CAM else WIDE_CAM
|
||||
road_camera_view.switch_stream(stream)
|
||||
road_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
finally:
|
||||
road_camera_view.close()
|
||||
|
||||
@@ -14,7 +14,7 @@ class DriverCameraDialog(CameraView):
|
||||
super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER)
|
||||
self.driver_state_renderer = DriverStateRenderer()
|
||||
# TODO: this can grow unbounded, should be given some thought
|
||||
device.add_interactive_timeout_callback(lambda: gui_app.set_modal_overlay(None))
|
||||
device.add_interactive_timeout_callback(gui_app.pop_widget)
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", True)
|
||||
|
||||
def hide_event(self):
|
||||
@@ -24,7 +24,7 @@ class DriverCameraDialog(CameraView):
|
||||
|
||||
def _handle_mouse_release(self, _):
|
||||
super()._handle_mouse_release(_)
|
||||
gui_app.set_modal_overlay(None)
|
||||
gui_app.pop_widget()
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
@@ -103,9 +103,9 @@ if __name__ == "__main__":
|
||||
gui_app.init_window("Driver Camera View")
|
||||
|
||||
driver_camera_view = DriverCameraDialog()
|
||||
gui_app.push_widget(driver_camera_view)
|
||||
try:
|
||||
for _ in gui_app.render():
|
||||
ui_state.update()
|
||||
driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
finally:
|
||||
driver_camera_view.close()
|
||||
|
||||
@@ -63,7 +63,7 @@ class ExpButton(Widget):
|
||||
|
||||
texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel
|
||||
rl.draw_circle(center_x, center_y, self._rect.width / 2, self._black_bg)
|
||||
rl.draw_texture(texture, center_x - texture.width // 2, center_y - texture.height // 2, self._white_color)
|
||||
rl.draw_texture_ex(texture, rl.Vector2(center_x - texture.width / 2, center_y - texture.height / 2), 0.0, 1.0, self._white_color)
|
||||
|
||||
def _held_or_actual_mode(self):
|
||||
now = time.monotonic()
|
||||
|
||||
@@ -86,7 +86,7 @@ class HudRenderer(Widget):
|
||||
|
||||
v_cruise_cluster = car_state.vCruiseCluster
|
||||
self.set_speed = (
|
||||
controls_state.vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster
|
||||
controls_state.deprecated.vCruise if v_cruise_cluster == 0.0 else v_cruise_cluster
|
||||
)
|
||||
self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA
|
||||
self.is_cruise_available = self.set_speed != -1
|
||||
|
||||
Executable → Regular
+16
-36
@@ -1,55 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.hardware import TICI
|
||||
from openpilot.common.realtime import config_realtime_process, set_core_affinity
|
||||
from openpilot.common.watchdog import kick_watchdog
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.selfdrive.ui.stall_monitor import UIStallMonitor
|
||||
from openpilot.selfdrive.ui.layouts.main import MainLayout
|
||||
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
BIG_UI = gui_app.big_ui()
|
||||
|
||||
|
||||
def main():
|
||||
cores = {5, }
|
||||
config_realtime_process(0, 51)
|
||||
|
||||
gui_app.init_window("UI")
|
||||
if gui_app.big_ui():
|
||||
main_layout = MainLayout()
|
||||
if BIG_UI:
|
||||
from openpilot.selfdrive.ui.layouts.main import MainLayout
|
||||
MainLayout()
|
||||
else:
|
||||
main_layout = MiciMainLayout()
|
||||
main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
stall_monitor = UIStallMonitor("raylib_ui")
|
||||
gui_app.set_progress_hook(stall_monitor.progress)
|
||||
stall_monitor.progress("ui.loop_ready")
|
||||
stall_monitor.start()
|
||||
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||
MiciMainLayout()
|
||||
|
||||
def render_layout() -> None:
|
||||
stall_monitor.progress("ui.before_layout_render")
|
||||
main_layout.render()
|
||||
stall_monitor.progress("ui.after_layout_render")
|
||||
|
||||
try:
|
||||
for should_render in gui_app.render(render_callback=render_layout):
|
||||
stall_monitor.progress("ui.loop_iteration")
|
||||
kick_watchdog()
|
||||
stall_monitor.progress("ui.after_watchdog")
|
||||
ui_state.update()
|
||||
stall_monitor.progress("ui.after_state_update")
|
||||
if should_render:
|
||||
# reaffine after power save offlines our core
|
||||
if TICI and os.sched_getaffinity(0) != cores:
|
||||
try:
|
||||
set_core_affinity(list(cores))
|
||||
except OSError:
|
||||
pass
|
||||
stall_monitor.progress("ui.loop_idle")
|
||||
finally:
|
||||
gui_app.set_progress_hook(None)
|
||||
stall_monitor.stop()
|
||||
for should_render in gui_app.render():
|
||||
ui_state.update()
|
||||
if should_render:
|
||||
# reaffine after power save offlines our core
|
||||
if TICI and os.sched_getaffinity(0) != cores:
|
||||
try:
|
||||
set_core_affinity(list(cores))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -25,6 +25,52 @@ from openpilot.system.ui.widgets.list_view import (
|
||||
VALUE_FONT_SIZE = 48
|
||||
|
||||
|
||||
class SshKeyFetcher:
|
||||
HTTP_TIMEOUT = 15 # seconds
|
||||
|
||||
def __init__(self, params: Params):
|
||||
self._params = params
|
||||
self._on_response: Callable[[str | None], None] | None = None
|
||||
self._done = False
|
||||
self._error: str | None = None
|
||||
|
||||
def fetch(self, username: str, on_response: Callable[[str | None], None]):
|
||||
self._error = None
|
||||
self._done = False
|
||||
self._on_response = on_response
|
||||
threading.Thread(target=self._fetch_thread, args=(username,), daemon=True).start()
|
||||
|
||||
def update(self):
|
||||
if not self._done:
|
||||
return
|
||||
self._done = False
|
||||
if self._error is not None:
|
||||
self.clear()
|
||||
if self._on_response:
|
||||
self._on_response(self._error)
|
||||
|
||||
def clear(self):
|
||||
self._params.remove("GithubUsername")
|
||||
self._params.remove("GithubSshKeys")
|
||||
|
||||
def _fetch_thread(self, username: str):
|
||||
try:
|
||||
response = requests.get(f"https://github.com/{username}.keys", timeout=self.HTTP_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
keys = response.text.strip()
|
||||
if not keys:
|
||||
raise requests.exceptions.HTTPError("No SSH keys found")
|
||||
|
||||
self._params.put("GithubUsername", username)
|
||||
self._params.put("GithubSshKeys", keys)
|
||||
except requests.exceptions.Timeout:
|
||||
self._error = tr("Request timed out")
|
||||
except Exception:
|
||||
self._error = tr("No SSH keys found for user '{}'").format(username)
|
||||
finally:
|
||||
self._done = True
|
||||
|
||||
|
||||
class SshKeyActionState(Enum):
|
||||
LOADING = tr_noop("LOADING")
|
||||
ADD = tr_noop("ADD")
|
||||
|
||||
Reference in New Issue
Block a user