mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-17 15:22:10 +08:00
567 lines
21 KiB
Python
567 lines
21 KiB
Python
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.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.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.selfdrive.ui.ui_state import ui_state
|
|
from openpilot.system.ui.widgets.label import MiciLabel
|
|
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
|
|
|
|
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):
|
|
content_height = self._content.get_total_height(int(rect.width))
|
|
content_height += self._fcc_logo.height + 20
|
|
|
|
scroll_content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
|
|
scroll_offset = round(self._scroll_panel.update(rect, scroll_content_rect.height))
|
|
|
|
fcc_pos = rl.Vector2(rect.x + 20, rect.y + 20 + scroll_offset)
|
|
|
|
scroll_content_rect.y += scroll_offset + self._fcc_logo.height + 20
|
|
self._content.render(scroll_content_rect)
|
|
|
|
rl.draw_texture_ex(self._fcc_logo, fcc_pos, 0.0, 1.0, rl.WHITE)
|
|
|
|
return -1
|
|
|
|
|
|
def _engaged_confirmation_callback(callback: Callable, action_text: str):
|
|
if not ui_state.engaged:
|
|
def confirm_callback():
|
|
# Check engaged again in case it changed while the dialog was open
|
|
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)
|
|
else:
|
|
dlg = BigDialog(f"Disengage to {action_text}", "")
|
|
gui_app.set_modal_overlay(dlg)
|
|
|
|
|
|
class DeviceInfoLayoutMici(Widget):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
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._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)
|
|
|
|
def _render(self, _):
|
|
self._dongle_id_label.set_position(self._rect.x + 20, self._rect.y - 10)
|
|
self._dongle_id_label.render()
|
|
|
|
self._dongle_id_text_label.set_position(self._rect.x + 20, self._rect.y + 68 - 25)
|
|
self._dongle_id_text_label.render()
|
|
|
|
self._serial_number_label.set_position(self._rect.x + 20, self._rect.y + 114 - 30)
|
|
self._serial_number_label.render()
|
|
|
|
self._serial_number_text_label.set_position(self._rect.x + 20, self._rect.y + 161 - 25)
|
|
self._serial_number_text_label.render()
|
|
|
|
|
|
class UpdaterState(IntEnum):
|
|
IDLE = 0
|
|
WAITING_FOR_UPDATER = 1
|
|
UPDATER_RESPONDING = 2
|
|
|
|
|
|
class PairBigButton(BigButton):
|
|
def __init__(self):
|
|
super().__init__("pair", "connect.comma.ai", "icons_mici/settings/comma_icon.png")
|
|
|
|
def _update_state(self):
|
|
if ui_state.prime_state.is_paired():
|
|
self.set_text("paired")
|
|
if ui_state.prime_state.is_prime():
|
|
self.set_value("subscribed")
|
|
else:
|
|
self.set_value("upgrade to prime")
|
|
else:
|
|
self.set_text("pair")
|
|
self.set_value("connect.comma.ai")
|
|
|
|
def _handle_mouse_release(self, mouse_pos: MousePos):
|
|
super()._handle_mouse_release(mouse_pos)
|
|
|
|
# TODO: show ad dialog when clicked if not prime
|
|
if ui_state.prime_state.is_paired():
|
|
return
|
|
dlg: BigDialog | PairingDialog
|
|
if not system_time_valid():
|
|
dlg = BigDialog(tr("Please connect to Wi-Fi to complete initial pairing"), "")
|
|
elif UNREGISTERED_DONGLE_ID == (ui_state.params.get("DongleId") or UNREGISTERED_DONGLE_ID):
|
|
dlg = BigDialog(tr("Device must be registered with 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()) / "frogpilot" / "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")
|
|
|
|
|
|
UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond
|
|
|
|
|
|
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_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
|
|
|
|
ui_state.add_offroad_transition_callback(self.offroad_transition)
|
|
|
|
def offroad_transition(self):
|
|
if ui_state.is_offroad():
|
|
self.set_enabled(True)
|
|
|
|
def _handle_mouse_release(self, mouse_pos: MousePos):
|
|
if not system_time_valid():
|
|
dlg = BigDialog(tr("Please connect to Wi-Fi to update"), "")
|
|
gui_app.set_modal_overlay(dlg)
|
|
return
|
|
|
|
self.set_enabled(False)
|
|
self._state = UpdaterState.WAITING_FOR_UPDATER
|
|
self.set_icon(self._txt_update_icon)
|
|
|
|
def run():
|
|
if self.get_value() == "download update":
|
|
os.system("pkill -SIGHUP -f system.updated.updated")
|
|
elif self.get_value() == "update now":
|
|
ui_state.params.put_bool("DoReboot", True)
|
|
else:
|
|
os.system("pkill -SIGUSR1 -f system.updated.updated")
|
|
|
|
threading.Thread(target=run, daemon=True).start()
|
|
|
|
def set_value(self, value: str):
|
|
super().set_value(value)
|
|
if value:
|
|
self.set_text("")
|
|
else:
|
|
self.set_text("update openpilot")
|
|
|
|
def _update_state(self):
|
|
if ui_state.started:
|
|
self.set_enabled(False)
|
|
return
|
|
|
|
updater_state = ui_state.params.get("UpdaterState") or ""
|
|
failed_count = ui_state.params.get("UpdateFailedCount")
|
|
failed = False if failed_count is None else int(failed_count) > 0
|
|
|
|
if ui_state.params.get_bool("UpdateAvailable"):
|
|
self.set_rotate_icon(False)
|
|
self.set_enabled(True)
|
|
if self.get_value() != "update now":
|
|
self.set_value("update now")
|
|
self.set_icon(self._txt_reboot_icon)
|
|
|
|
elif self._state == UpdaterState.WAITING_FOR_UPDATER:
|
|
self.set_rotate_icon(True)
|
|
if updater_state != "idle":
|
|
self._state = UpdaterState.UPDATER_RESPONDING
|
|
|
|
# Recover from updater not responding (time invalid shortly after boot)
|
|
if self._waiting_for_updater_t is None:
|
|
self._waiting_for_updater_t = rl.get_time()
|
|
|
|
if self._waiting_for_updater_t is not None and rl.get_time() - self._waiting_for_updater_t > UPDATER_TIMEOUT:
|
|
self.set_rotate_icon(False)
|
|
self.set_value("updater failed to respond")
|
|
self._state = UpdaterState.IDLE
|
|
self._hide_value_t = rl.get_time()
|
|
|
|
elif self._state == UpdaterState.UPDATER_RESPONDING:
|
|
if updater_state == "idle":
|
|
self.set_rotate_icon(False)
|
|
self._state = UpdaterState.IDLE
|
|
self._hide_value_t = rl.get_time()
|
|
else:
|
|
if self.get_value() != updater_state:
|
|
self.set_value(updater_state)
|
|
|
|
elif self._state == UpdaterState.IDLE:
|
|
self.set_rotate_icon(False)
|
|
if failed:
|
|
if self.get_value() != "failed to update":
|
|
self.set_value("failed to update")
|
|
|
|
elif ui_state.params.get_bool("UpdaterFetchAvailable"):
|
|
self.set_enabled(True)
|
|
if self.get_value() != "download update":
|
|
self.set_value("download update")
|
|
|
|
elif self._hide_value_t is not None:
|
|
self.set_enabled(True)
|
|
if self.get_value() == "checking...":
|
|
self.set_value("up to date")
|
|
self.set_icon(self._txt_up_to_date_icon)
|
|
|
|
# Hide previous text after short amount of time (up to date or failed)
|
|
if rl.get_time() - self._hide_value_t > 3.0:
|
|
self._hide_value_t = None
|
|
self.set_value("")
|
|
self.set_icon(self._txt_update_icon)
|
|
else:
|
|
if self.get_value() != "":
|
|
self.set_value("")
|
|
|
|
if self._state != UpdaterState.WAITING_FOR_UPDATER:
|
|
self._waiting_for_updater_t = None
|
|
|
|
|
|
class DeviceLayoutMici(NavWidget):
|
|
def __init__(self, back_callback: Callable):
|
|
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)
|
|
|
|
def reboot_callback():
|
|
ui_state.params.put_bool("DoReboot", True)
|
|
|
|
def reset_calibration_callback():
|
|
params = ui_state.params
|
|
params.remove("CalibrationParams")
|
|
params.remove("LiveTorqueParameters")
|
|
params.remove("LiveParameters")
|
|
params.remove("LiveParametersV2")
|
|
params.remove("LiveDelay")
|
|
params.put_bool("OnroadCycleRequested", True)
|
|
|
|
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 = 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 = 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 = 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 = 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.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.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.set_enabled(lambda: ui_state.is_offroad())
|
|
|
|
self._scroller = Scroller([
|
|
DeviceInfoLayoutMici(),
|
|
UpdateOpenpilotBigButton(),
|
|
BigParamControl("automatically update starpilot", "AutomaticUpdates"),
|
|
PairBigButton(),
|
|
review_training_guide_btn,
|
|
driver_cam_btn,
|
|
reset_driver_monitoring_btn,
|
|
# lang_button,
|
|
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)
|