Files
StarPilot/selfdrive/ui/mici/layouts/settings/software.py
T
firestar5683 66e04d4a45 KA THIS CHOW
2026-07-01 22:59:38 -05:00

316 lines
11 KiB
Python

import os
import threading
from collections.abc import Callable
from enum import IntEnum
from functools import cache
from pathlib import Path
import pyray as rl
from openpilot.common.time_helpers import system_time_valid
from openpilot.selfdrive.ui.lib.starpilot_version import STARPILOT_DISPLAY_VERSION
from openpilot.selfdrive.ui.mici.layouts.settings.device import EngagedConfirmationButton
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigParamControl
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.hardware.hw import Paths
from openpilot.system.ui.lib.application import FontWeight, MousePos, gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.scroller import NavScroller
UPDATER_TIMEOUT = 10.0
@cache
def _is_frogs_go_moo() -> bool:
return (Path(Paths.persist_root()) / "frogsgomoo.py").is_file()
def _split_description(desc: str) -> tuple[str, str, str, str] | None:
parts = [p.strip() for p in desc.split(" / ")]
if len(parts) != 4:
return None
version, branch, commit, date = parts
return version, branch, commit, date
def _update_failed() -> bool:
failed_count = ui_state.params.get("UpdateFailedCount")
try:
return False if failed_count is None else int(failed_count) > 0
except (TypeError, ValueError):
return False
def _request_update_check():
ui_state.params_memory.put_bool("ManualUpdateInitiated", True)
os.system("pkill -SIGUSR1 -f system.updated.updated")
def _request_update_download():
ui_state.params_memory.put_bool("ManualUpdateInitiated", True)
os.system("pkill -SIGHUP -f system.updated.updated")
class UpdaterState(IntEnum):
IDLE = 0
WAITING_FOR_UPDATER = 1
UPDATER_RESPONDING = 2
class SoftwareInfoLayoutMici(Widget):
def __init__(self):
super().__init__()
self.set_rect(rl.Rectangle(0, 0, 360, 180))
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
max_width = int(self._rect.width - 20)
self._version_label = UnifiedLabel("version", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False)
self._version_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color,
font_weight=FontWeight.ROMAN, wrap_text=False)
self._branch_label = UnifiedLabel("branch", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False)
self._branch_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color,
font_weight=FontWeight.ROMAN, wrap_text=False)
def _update_state(self):
desc = _split_description(ui_state.params.get("UpdaterCurrentDescription") or "")
if desc is not None:
_, branch, commit, date = desc
self._version_text_label.set_text(f"{STARPILOT_DISPLAY_VERSION} ({date})")
self._branch_text_label.set_text(f"{branch} ({commit})")
else:
self._version_text_label.set_text(STARPILOT_DISPLAY_VERSION if ui_state.params.get("Version") else "N/A")
self._branch_text_label.set_text(ui_state.params.get("GitBranch") or "N/A")
def _render(self, _):
self._version_label.set_position(self._rect.x + 20, self._rect.y - 10)
self._version_label.render()
self._version_text_label.set_position(self._rect.x + 20, self._rect.y + 68 - 25)
self._version_text_label.render()
self._branch_label.set_position(self._rect.x + 20, self._rect.y + 114 - 30)
self._branch_label.render()
self._branch_text_label.set_position(self._rect.x + 20, self._rect.y + 161 - 25)
self._branch_text_label.render()
class CheckUpdateButton(BigButton):
def __init__(self):
self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75)
self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
super().__init__("check for update", "", 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":
_request_update_download()
else:
_request_update_check()
threading.Thread(target=run, daemon=True).start()
def set_value(self, value: str):
super().set_value(value)
self.set_text("" if value else "check for update")
def _update_state(self):
super()._update_state()
if ui_state.started:
self.set_enabled(False)
return
updater_state = ui_state.params.get("UpdaterState") or ""
if self._state == UpdaterState.WAITING_FOR_UPDATER:
self.set_rotate_icon(True)
if updater_state != "idle":
self._state = UpdaterState.UPDATER_RESPONDING
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()
elif self.get_value() != updater_state:
self.set_value(updater_state)
elif self._state == UpdaterState.IDLE:
self.set_rotate_icon(False)
if _update_failed():
self.set_enabled(True)
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)
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)
elif self.get_value() != "":
self.set_value("")
if self._state != UpdaterState.WAITING_FOR_UPDATER:
self._waiting_for_updater_t = None
class InstallUpdateButton(BigButton):
def __init__(self):
super().__init__("install update", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70))
self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable"))
def _update_state(self):
super()._update_state()
desc = _split_description(ui_state.params.get("UpdaterNewDescription") or "")
value = f"{STARPILOT_DISPLAY_VERSION} ({desc[1]})" if desc is not None else ""
if self.get_value() != value:
self.set_value(value)
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
self.set_enabled(False)
def run():
ui_state.params.put_bool("DoReboot", True)
threading.Thread(target=run, daemon=True).start()
class BranchSelectPage(NavScroller):
def __init__(self, on_select: Callable[[str], None]):
super().__init__()
params = ui_state.params
current_git_branch = params.get("GitBranch") or ""
branches_str = params.get("UpdaterAvailableBranches") or ""
branches = [b for b in branches_str.split(",") if b]
if not _is_frogs_go_moo():
for hidden_branch in ("StarPilot-Vetting", "MAKE-PRS-HERE"):
if hidden_branch in branches:
branches.remove(hidden_branch)
for branch in [current_git_branch, "devel-staging", "devel", "nightly", "nightly-dev", "master"]:
if branch in branches:
branches.remove(branch)
branches.insert(0, branch)
current_target = params.get("UpdaterTargetBranch") or ""
check_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
buttons = []
if not branches:
btn = BigButton("no branches", "check for update first", scroll=True)
btn.set_enabled(False)
buttons.append(btn)
else:
for branch in branches:
btn = BigButton(branch, "", check_icon if branch == current_target else None, scroll=True)
btn.set_click_callback(lambda b=branch: self.dismiss(lambda: on_select(b)))
buttons.append(btn)
self._scroller.add_widgets(buttons)
class TargetBranchButton(BigButton):
def __init__(self):
super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "")
self._download_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75)
self.set_click_callback(self._on_click)
self.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
self.set_enabled(lambda: ui_state.is_offroad())
def _update_state(self):
super()._update_state()
target = ui_state.params.get("UpdaterTargetBranch") or ""
if self.get_value() != target:
self.set_value(target)
def _on_click(self):
gui_app.push_widget(BranchSelectPage(self._on_select))
def _on_select(self, branch: str):
previous = ui_state.params.get("UpdaterTargetBranch") or ""
ui_state.params.put("UpdaterTargetBranch", branch)
self.set_value(branch)
_request_update_check()
if branch != previous:
gui_app.push_widget(BigConfirmationDialog("slide to\ndownload", self._download_icon, _request_update_download))
class SoftwareLayoutMici(NavScroller):
def __init__(self):
super().__init__()
def uninstall_openpilot_callback():
ui_state.params.put_bool("DoUninstall", True)
self._automatic_updates_btn = BigParamControl("auto updates", "AutomaticUpdates")
uninstall_openpilot_btn = EngagedConfirmationButton("uninstall\nStarPilot", "uninstall",
gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64),
uninstall_openpilot_callback, exit_on_confirm=False)
self._scroller.add_widgets([
SoftwareInfoLayoutMici(),
CheckUpdateButton(),
InstallUpdateButton(),
TargetBranchButton(),
self._automatic_updates_btn,
uninstall_openpilot_btn,
])
def _update_state(self):
super()._update_state()
self._automatic_updates_btn.refresh()