mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-09 07:32:06 +08:00
447 lines
17 KiB
Python
447 lines
17 KiB
Python
import os
|
|
import threading
|
|
import pyray as rl
|
|
from enum import IntEnum
|
|
from pathlib import Path
|
|
from collections.abc import Callable
|
|
|
|
from openpilot.common.basedir import BASEDIR
|
|
from openpilot.common.params import Params
|
|
from openpilot.common.time_helpers import system_time_valid
|
|
from openpilot.system.hardware import PC
|
|
from openpilot.system.hardware.hw import Paths
|
|
from openpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller
|
|
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton, BigParamControl
|
|
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigMultiOptionDialog
|
|
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog, get_pairing_backend_name, get_pairing_host
|
|
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
|
|
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
|
|
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
|
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.starpilot.common.connect_server import prepare_konik_server_switch
|
|
|
|
|
|
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._content = HtmlRenderer(file_path=file_path, text=text)
|
|
self._fcc_logo = gui_app.texture("icons_mici/settings/device/fcc_logo.png", 76, 64)
|
|
|
|
def _render(self, rect: rl.Rectangle):
|
|
content_height = self._content.get_total_height(int(rect.width))
|
|
content_height += self._fcc_logo.height + 20
|
|
|
|
scroll_content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
|
|
scroll_offset = round(self._scroll_panel.update(rect, scroll_content_rect.height))
|
|
|
|
fcc_pos = rl.Vector2(rect.x + 20, rect.y + 20 + scroll_offset)
|
|
|
|
scroll_content_rect.y += scroll_offset + self._fcc_logo.height + 20
|
|
self._content.render(scroll_content_rect)
|
|
|
|
rl.draw_texture_ex(self._fcc_logo, fcc_pos, 0.0, 1.0, rl.WHITE)
|
|
|
|
|
|
def _engaged_confirmation_click(callback: Callable, action_text: str, icon: rl.Texture, exit_on_confirm: bool = True, red: bool = False):
|
|
if not ui_state.engaged:
|
|
def confirm_callback():
|
|
# Check engaged again in case it changed while the dialog was open
|
|
# TODO: if true, we stay on the dialog if not exit_on_confirm until normal onroad timeout
|
|
if not ui_state.engaged:
|
|
callback()
|
|
|
|
gui_app.push_widget(BigConfirmationDialog(f"slide to\n{action_text.lower()}", icon, confirm_callback, exit_on_confirm=exit_on_confirm, red=red))
|
|
else:
|
|
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):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
self.set_rect(rl.Rectangle(0, 0, 360, 180))
|
|
|
|
params = Params()
|
|
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
|
|
max_width = int(self._rect.width - 20)
|
|
self._dongle_id_label = UnifiedLabel("device ID", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False)
|
|
self._dongle_id_text_label = UnifiedLabel(params.get("DongleId") or 'N/A', 32, max_width=max_width, text_color=subheader_color,
|
|
font_weight=FontWeight.ROMAN, wrap_text=False)
|
|
|
|
self._serial_number_label = UnifiedLabel("serial", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False)
|
|
self._serial_number_text_label = UnifiedLabel(params.get("HardwareSerial") or 'N/A', 32, max_width=max_width, text_color=subheader_color,
|
|
font_weight=FontWeight.ROMAN, wrap_text=False)
|
|
|
|
def _render(self, _):
|
|
self._dongle_id_label.set_position(self._rect.x + 20, self._rect.y - 10)
|
|
self._dongle_id_label.render()
|
|
|
|
self._dongle_id_text_label.set_position(self._rect.x + 20, self._rect.y + 68 - 25)
|
|
self._dongle_id_text_label.render()
|
|
|
|
self._serial_number_label.set_position(self._rect.x + 20, self._rect.y + 114 - 30)
|
|
self._serial_number_label.render()
|
|
|
|
self._serial_number_text_label.set_position(self._rect.x + 20, self._rect.y + 161 - 25)
|
|
self._serial_number_text_label.render()
|
|
|
|
|
|
class UpdaterState(IntEnum):
|
|
IDLE = 0
|
|
WAITING_FOR_UPDATER = 1
|
|
UPDATER_RESPONDING = 2
|
|
|
|
|
|
def _konik_toggle_path() -> Path:
|
|
return Path(Paths.comma_home()) / "starpilot" / "cache" / "use_konik" if PC else Path("/cache/use_konik")
|
|
|
|
|
|
class ConnectServerBigButton(BigButton):
|
|
_COMMA_OPTION = "comma connect"
|
|
_KONIK_OPTION = "konik connect"
|
|
|
|
def __init__(self):
|
|
self._params = Params()
|
|
self._reboot_icon = gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70)
|
|
super().__init__("connect\nserver", get_pairing_host(self._params), gui_app.texture("icons_mici/settings/comma_icon.png", 33, 60))
|
|
|
|
def _get_label_font_size(self):
|
|
return 52
|
|
|
|
def _selected_option(self) -> str:
|
|
return self._KONIK_OPTION if self._params.get_bool("UseKonikServer") else self._COMMA_OPTION
|
|
|
|
def _apply_selection(self, selection: str):
|
|
use_konik = selection == self._KONIK_OPTION
|
|
prepare_konik_server_switch(use_konik, self._params)
|
|
|
|
toggle_path = _konik_toggle_path()
|
|
toggle_path.parent.mkdir(parents=True, exist_ok=True)
|
|
if use_konik:
|
|
toggle_path.touch(exist_ok=True)
|
|
else:
|
|
try:
|
|
toggle_path.unlink(missing_ok=True)
|
|
except TypeError:
|
|
if toggle_path.exists():
|
|
toggle_path.unlink()
|
|
|
|
ui_state.params.put_bool("DoReboot", True)
|
|
|
|
def _handle_mouse_release(self, mouse_pos: MousePos):
|
|
super()._handle_mouse_release(mouse_pos)
|
|
|
|
dialog_holder: dict[str, BigMultiOptionDialog] = {}
|
|
|
|
def on_confirm():
|
|
selection = dialog_holder["dialog"].get_selected_option()
|
|
if selection == self._selected_option():
|
|
return
|
|
|
|
gui_app.push_widget(BigConfirmationDialog("slide to\nreboot", self._reboot_icon, lambda: self._apply_selection(selection)))
|
|
|
|
dialog = BigMultiOptionDialog(options=[self._COMMA_OPTION, self._KONIK_OPTION], default=self._selected_option(), right_btn_callback=on_confirm)
|
|
dialog_holder["dialog"] = dialog
|
|
gui_app.push_widget(dialog)
|
|
|
|
def _update_state(self):
|
|
super()._update_state()
|
|
self.set_value(get_pairing_host(self._params))
|
|
|
|
|
|
class PairBigButton(BigButton):
|
|
def __init__(self):
|
|
super().__init__("pair", get_pairing_host(ui_state.params), 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():
|
|
self.set_value("subscribed")
|
|
else:
|
|
self.set_value("upgrade to prime")
|
|
else:
|
|
self.set_text("pair")
|
|
self.set_value(get_pairing_host(ui_state.params))
|
|
|
|
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("", f"Device must be registered with the {get_pairing_backend_name(ui_state.params)} backend to pair.")
|
|
else:
|
|
dlg = PairingDialog()
|
|
gui_app.push_widget(dlg)
|
|
|
|
|
|
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, 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
|
|
|
|
ui_state.add_offroad_transition_callback(self.offroad_transition)
|
|
|
|
def offroad_transition(self):
|
|
if ui_state.is_offroad():
|
|
self.set_enabled(True)
|
|
|
|
def _handle_mouse_release(self, mouse_pos: MousePos):
|
|
super()._handle_mouse_release(mouse_pos)
|
|
|
|
if not system_time_valid():
|
|
dlg = BigDialog("", tr("Please connect to Wi-Fi to update."))
|
|
gui_app.push_widget(dlg)
|
|
return
|
|
|
|
self.set_enabled(False)
|
|
self._state = UpdaterState.WAITING_FOR_UPDATER
|
|
self.set_icon(self._txt_update_icon)
|
|
|
|
def run():
|
|
if self.get_value() == "download update":
|
|
os.system("pkill -SIGHUP -f system.updated.updated")
|
|
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):
|
|
super()._update_state()
|
|
|
|
if ui_state.started:
|
|
self.set_enabled(False)
|
|
return
|
|
|
|
updater_state = ui_state.params.get("UpdaterState") or ""
|
|
failed_count = ui_state.params.get("UpdateFailedCount")
|
|
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\nto respond")
|
|
self._state = UpdaterState.IDLE
|
|
self._hide_value_t = rl.get_time()
|
|
|
|
elif self._state == UpdaterState.UPDATER_RESPONDING:
|
|
if updater_state == "idle":
|
|
self.set_rotate_icon(False)
|
|
self._state = UpdaterState.IDLE
|
|
self._hide_value_t = rl.get_time()
|
|
else:
|
|
if self.get_value() != 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(NavScroller):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
self._fcc_dialog: HtmlModal | None = None
|
|
|
|
def power_off_callback():
|
|
ui_state.params.put_bool("DoShutdown", True)
|
|
|
|
def reboot_callback():
|
|
ui_state.params.put_bool("DoReboot", True)
|
|
|
|
def reset_calibration_callback():
|
|
params = ui_state.params
|
|
params.remove("CalibrationParams")
|
|
params.remove("LiveTorqueParameters")
|
|
params.remove("LiveParameters")
|
|
params.remove("LiveParametersV2")
|
|
params.remove("LiveDelay")
|
|
params.put_bool("OnroadCycleRequested", True)
|
|
|
|
def reset_driver_monitoring_callback():
|
|
params = ui_state.params
|
|
params.remove("IsRhdDetected")
|
|
params.remove("IsRHD")
|
|
params.remove("IsRHDOverride")
|
|
params.put_bool("OnroadCycleRequested", True)
|
|
|
|
def uninstall_openpilot_callback():
|
|
ui_state.params.put_bool("DoUninstall", True)
|
|
|
|
reset_driver_monitoring_btn = EngagedConfirmationButton(
|
|
"reset driver\nmonitoring",
|
|
"reset driver monitoring",
|
|
gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64),
|
|
reset_driver_monitoring_callback,
|
|
)
|
|
|
|
reset_calibration_btn = EngagedConfirmationButton("reset calibration", "reset", gui_app.texture("icons_mici/settings/device/lkas.png", 122, 64),
|
|
reset_calibration_callback)
|
|
|
|
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)
|
|
|
|
reboot_btn = EngagedConfirmationCircleButton("reboot", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70),
|
|
reboot_callback, exit_on_confirm=False)
|
|
|
|
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)
|
|
|
|
regulatory_btn = BigButton("regulatory info", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64))
|
|
regulatory_btn.set_click_callback(self._on_regulatory)
|
|
|
|
self._connect_server_btn = ConnectServerBigButton()
|
|
self._simple_mode_btn = BigParamControl("simple mode", "SimpleMode")
|
|
|
|
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\ntraining guide", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64))
|
|
review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTrainingGuide(completed_callback=lambda: gui_app.pop_widgets_to(self))))
|
|
review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad())
|
|
|
|
terms_btn = BigButton("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(),
|
|
self._connect_server_btn,
|
|
PairBigButton(),
|
|
self._simple_mode_btn,
|
|
review_training_guide_btn,
|
|
driver_cam_btn,
|
|
reset_driver_monitoring_btn,
|
|
terms_btn,
|
|
regulatory_btn,
|
|
reset_calibration_btn,
|
|
uninstall_openpilot_btn,
|
|
reboot_btn,
|
|
self._power_off_btn,
|
|
])
|
|
|
|
def show_event(self):
|
|
super().show_event()
|
|
self._simple_mode_btn.refresh()
|
|
|
|
def _update_state(self):
|
|
super()._update_state()
|
|
self._simple_mode_btn.refresh()
|
|
|
|
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.push_widget(self._fcc_dialog)
|